From ebbaa839a80de140b2f522bab66427d2e0015e4f Mon Sep 17 00:00:00 2001 From: Caner <s86215@bht-berlin.de> Date: Tue, 6 Jun 2023 14:19:28 +0200 Subject: [PATCH] fix --- .env.example | 14 +- app/(auth)/login/page.tsx | 51 +++++-- app/(auth)/signup/page.tsx | 62 ++++++-- app/(content)/(home)/home/page.tsx | 4 +- app/(content)/followers/page.tsx | 11 +- app/api/auth/[...nextauth]/route.ts | 80 +--------- app/api/likes/likeService.ts | 12 +- app/api/messages/route.ts | 22 +-- app/api/route.ts | 9 +- app/api/signup/route.ts | 54 +++++-- app/api/user/[userid].ts | 28 ---- app/api/user/index.ts | 22 --- app/layout.tsx | 2 + components/auth-login-form.tsx | 69 --------- components/auth-signup-form.tsx | 3 - components/icons.tsx | 8 +- components/logo.tsx | 10 ++ components/nav.tsx | 22 +-- components/ui/toast.tsx | 2 +- components/ui/toaster.tsx | 35 +++++ components/ui/use-toast.ts | 4 +- components/user-auth-form.tsx | 224 ++++++++++++++++++++++++++++ components/user-item.tsx | 6 +- env.mjs | 33 ++++ lib/auth.ts | 99 ++++++++++++ lib/db.ts | 23 +-- lib/igdb.ts | 9 +- lib/session.ts | 9 ++ lib/utils.ts | 4 +- lib/validations/auth.ts | 14 +- next.config.js => next.config.mjs | 6 +- package-lock.json | 170 +++++++++++++++++---- package.json | 19 ++- prisma/schema.prisma | 109 +++++++++++--- tsconfig.json | 3 +- types/next-auth.d.ts | 22 ++- 36 files changed, 900 insertions(+), 374 deletions(-) delete mode 100644 app/api/user/[userid].ts delete mode 100644 app/api/user/index.ts delete mode 100644 components/auth-login-form.tsx create mode 100644 components/logo.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/user-auth-form.tsx create mode 100644 env.mjs create mode 100644 lib/auth.ts create mode 100644 lib/session.ts rename next.config.js => next.config.mjs (61%) diff --git a/.env.example b/.env.example index 31b2114..6736807 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,24 @@ # Example .env file +# Public App URL +NEXT_PUBLIC_APP_URL="http://localhost:3000" + # Database for connecting to Prisma DATABASE_URL="file:./dev.db" -# Some URLs +# URLs TWITCH_AUTH_BASE_URL="https://id.twitch.tv/oauth2" IGDB_BASE_URL="https://api.igdb.com/v4" IGDB_IMG_BASE_URL="https://images.igdb.com/igdb/image/upload" -# For Authentication +# For Twitch Auth to fetch access token TWITCH_CLIENT_ID="imdb_client_id" TWITCH_CLIENT_SECRET="imdb_auth_id" +# For NextAuth // use `openssl rand -base64 32` to generate a secret +NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="secret" -NEXTAUTH_URL="http://localhost:3000" \ No newline at end of file + +# For Github Auth +GITHUB_CLIENT_ID="github_client_id" +GITHUB_CLIENT_SECRET="github_client_secret" \ No newline at end of file diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index f499915..5cadd64 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,17 +1,48 @@ -import { LoginForm } from '@/components/auth-login-form' +import { Icons } from '@/components/icons' +import { GameUnityLogo } from '@/components/logo' +import { buttonVariants } from '@/components/ui/button' +import { UserAuthForm } from '@/components/user-auth-form' +import { cn } from '@/lib/utils' import Link from 'next/link' +export const metadata = { + title: "Login", + description: "Login to your account", +} + export default function LoginPage() { return ( - <div className="h-screen w-screen flex justify-center items-center bg-slate-100"> - <div className="sm:shadow-xl px-8 pb-8 pt-12 sm:bg-black rounded-xl space-y-12"> - <h1 className="font-semibold text-2xl">Login</h1> - <LoginForm /> - <p className="text-center"> - Need to create an account?{' '} - <Link className="text-indigo-500 hover:underline" href="/signup"> - Create Account - </Link>{' '} + <div className="container flex min-h-screen w-screen flex-col items-center justify-center"> + <Link + href="/" + className={cn( + buttonVariants({ variant: "ghost" }), + "absolute left-4 top-4 md:left-8 md:top-8" + )} + > + <> + <Icons.chevronLeft /> + Back + </> + </Link> + <div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"> + <div className="flex flex-col items-center space-y-2 text-center"> + <GameUnityLogo className="h-10 w-10" /> + <h1 className="text-2xl font-semibold tracking-tight"> + Welcome back + </h1> + <p className="text-sm text-muted-foreground"> + Enter your email to sign in to your account + </p> + </div> + <UserAuthForm type='login' /> + <p className="px-8 text-center text-sm text-muted-foreground"> + <Link + href="/signup" + className="hover:text-brand underline underline-offset-4" + > + Don't have an account? Sign Up + </Link> </p> </div> </div> diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index ea1b9f6..da3422f 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -1,18 +1,58 @@ -import { SignupForm } from '@/components/auth-signup-form' +import { GameUnityLogo } from '@/components/logo' +import { buttonVariants } from '@/components/ui/button' +import { UserAuthForm } from '@/components/user-auth-form' +import { cn } from '@/lib/utils' import Link from 'next/link' +export const metadata = { + title: "Create an account", + description: "Create an account to get started.", +} + export default function SignupPage() { return ( - <div className="h-screen w-screen flex justify-center items-center bg-slate-100"> - <div className="sm:shadow-xl px-8 pb-8 pt-12 sm:bg-black rounded-xl space-y-12"> - <h1 className="font-semibold text-2xl">Create your Account</h1> - <SignupForm /> - <p className="text-center"> - Have an account?{' '} - <Link className="text-indigo-500 hover:underline" href="/login"> - Sign in - </Link>{' '} - </p> + <div className="container grid h-screen w-screen flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0"> + <Link + href="/login" + className={cn( + buttonVariants({ variant: "ghost" }), + "absolute right-4 top-4 md:right-8 md:top-8" + )} + > + Login + </Link> + <div className="hidden h-full bg-muted lg:block" /> + <div className="lg:p-8"> + <div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"> + <div className="flex flex-col items-center space-y-2 text-center"> + <GameUnityLogo className="h-10 w-10" /> + + <h1 className="text-2xl font-semibold tracking-tight"> + Create an account + </h1> + <p className="text-sm text-muted-foreground"> + Give yourself a username, enter your email and password below to create an account + </p> + </div> + <UserAuthForm type='signup' /> + <p className="px-8 text-center text-sm text-muted-foreground"> + By clicking continue, you agree to our{" "} + <Link + href="/terms" + className="hover:text-brand underline underline-offset-4" + > + Terms of Service + </Link>{" "} + and{" "} + <Link + href="/privacy" + className="hover:text-brand underline underline-offset-4" + > + Privacy Policy + </Link> + . + </p> + </div> </div> </div> ) diff --git a/app/(content)/(home)/home/page.tsx b/app/(content)/(home)/home/page.tsx index 89b396f..d50aa7c 100644 --- a/app/(content)/(home)/home/page.tsx +++ b/app/(content)/(home)/home/page.tsx @@ -1,6 +1,6 @@ import LikeButton from "@/components/like-button"; import PostMessageForm from "@/components/post-messages"; -import { prisma } from "@/lib/db"; +import { db } from "@/lib/db"; import { Prisma } from "@prisma/client"; /* export const revalidate = 5; */ // revalidate this page every 60 seconds @@ -12,7 +12,7 @@ type messageItemProps = { export default async function HomePage() { let messages = null try { - messages = await prisma.post.findMany({ + messages = await db.post.findMany({ orderBy: { createdAt: "desc" } diff --git a/app/(content)/followers/page.tsx b/app/(content)/followers/page.tsx index 7fbb300..ec8175d 100644 --- a/app/(content)/followers/page.tsx +++ b/app/(content)/followers/page.tsx @@ -1,18 +1,17 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import FollowersList from "@/components/following-users"; import { getServerSession } from "next-auth"; export default async function Followers() { - const session = await getServerSession(authOptions); + // const session = await getServerSession(authOptions); - if (!session) { - return <div>Loading...</div>; - } + // if (!session) { + // return <div>Loading...</div>; + // } return ( <div> <h1>Followers Page WIP</h1> - <FollowersList userId={parseFloat(session.user?.id)} /> + {/* <FollowersList userId={parseFloat(session.user?.id)} /> */} </div> ) } \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index ca23bf0..17d6a45 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,79 +1,7 @@ -import { prisma } from '@/lib/db' -import { compare } from 'bcrypt' -import NextAuth, { type NextAuthOptions } from 'next-auth' -import CredentialsProvider from 'next-auth/providers/credentials' - -export const authOptions: NextAuthOptions = { - session: { - strategy: 'jwt' - }, - providers: [ - CredentialsProvider({ - name: 'Sign in', - credentials: { - email: { - label: 'Email', - type: 'email', - placeholder: 'hello@example.com' - }, - password: { label: 'Password', type: 'password' } - }, - async authorize(credentials) { - if (!credentials?.email || !credentials.password) { - return null - } - - const user = await prisma.user.findUnique({ - where: { - email: credentials.email - } - }) - - if (!user) { - return null - } - - const isPasswordValid = await compare( - credentials.password, - user.password - ) - - if (!isPasswordValid) { - return null - } - - return { - id: user.id + '', - email: user.email, - name: user.name, - } - } - }) - ], - callbacks: { - session: ({ session, token }) => { - console.log('Session Callback', { session, token }) - return { - ...session, - user: { - ...session.user, - id: token.id, - } - } - }, - jwt: ({ token, user }) => { - console.log('JWT Callback', { token, user }) - if (user) { - const u = user as unknown as any - return { - ...token, - id: u.id, - } - } - return token - } - } -} +import { authOptions } from '@/lib/auth' +import NextAuth from 'next-auth' const handler = NextAuth(authOptions) + export { handler as GET, handler as POST } + diff --git a/app/api/likes/likeService.ts b/app/api/likes/likeService.ts index 0dd748e..4353945 100644 --- a/app/api/likes/likeService.ts +++ b/app/api/likes/likeService.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/lib/db" +import { db } from "@/lib/db" import { Prisma } from "@prisma/client" type likeType = Prisma.LikeUncheckedCreateInput @@ -12,7 +12,7 @@ export async function putLike(like: likeType): Promise<likeType | undefined> { // if exists delete // if not create try { - const actualLike = await prisma.like.findFirst({ + const actualLike = await db.like.findFirst({ where: { id: like.id, postId: like.postId, @@ -25,13 +25,13 @@ export async function putLike(like: likeType): Promise<likeType | undefined> { throw Error("Message was not liked by this user") } - await prisma.like.delete({ + await db.like.delete({ where: { id: actualLike.id } }) - const msg = await prisma.post.update({ + const msg = await db.post.update({ where: { id: like.postId }, @@ -43,14 +43,14 @@ export async function putLike(like: likeType): Promise<likeType | undefined> { return undefined; } catch { - const createdLike = await prisma.like.create({ + const createdLike = await db.like.create({ data: { postId: like.postId, userId: like.userId } }) - const updatedMessage = await prisma.post.update({ + const updatedMessage = await db.post.update({ where: { id: like.postId }, diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts index c5eecd2..6abf1de 100644 --- a/app/api/messages/route.ts +++ b/app/api/messages/route.ts @@ -1,9 +1,9 @@ -import { prisma } from "@/lib/db" -import { NextRequest, NextResponse } from "next/server" -import { getServerSession } from "next-auth/next" -import { authOptions } from "../auth/[...nextauth]/route"; +import { authOptions } from "@/lib/auth"; +import { db } from "@/lib/db"; import { Prisma } from "@prisma/client"; -import { revalidatePath, revalidateTag } from "next/cache"; +import { getServerSession } from "next-auth/next"; +import { revalidatePath } from "next/cache"; +import { NextRequest, NextResponse } from "next/server"; type post = Prisma.PostUncheckedCreateInput @@ -20,11 +20,11 @@ export async function POST(req: NextRequest) { console.log("router data: " + data.content, "status:") try { - await prisma.post.create({ + await db.post.create({ /* data: data */ - data:{ + data: { content: data.content, - userId: parseInt(userId), + userId: userId, published: true } }) @@ -34,7 +34,7 @@ export async function POST(req: NextRequest) { return NextResponse.json({ status: 201, message: 'Message Created' }) - } catch (error) { + } catch (error: any) { console.log("fail" + error); } console.log("post") @@ -45,11 +45,11 @@ export async function GET(req: NextRequest, res: NextResponse) { const data = await req.json() console.log("router data: " + data, "status:") } catch (error) { - + } try { - const messages = await prisma.post.findMany({ + const messages = await db.post.findMany({ orderBy: { createdAt: "desc" } diff --git a/app/api/route.ts b/app/api/route.ts index 712fbf8..ae6ece8 100644 --- a/app/api/route.ts +++ b/app/api/route.ts @@ -1,16 +1,13 @@ +import { authOptions } from '@/lib/auth' import { getServerSession } from 'next-auth/next' import { NextResponse } from 'next/server' -import { authOptions } from './auth/[...nextauth]/route' -export async function GET(request: Request) { +export async function GET() { const session = await getServerSession(authOptions) if (!session) { - return new NextResponse(JSON.stringify({ error: 'unauthorized' }), { - status: 401 - }) + return new NextResponse(JSON.stringify({ error: 'unauthorized' }), { status: 401 }) } - console.log('GET API', session) return NextResponse.json({ authenticated: !!session }) } \ No newline at end of file diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts index 79937ac..124f2fd 100644 --- a/app/api/signup/route.ts +++ b/app/api/signup/route.ts @@ -1,32 +1,64 @@ -import { prisma } from '@/lib/db' +import { db } from '@/lib/db' import { hash } from 'bcrypt' import { NextResponse } from 'next/server' export async function POST(req: Request) { try { - const { email, password } = await req.json() + const { username, email, password } = await req.json() const hashed = await hash(password, 12) - const user = await prisma.user.create({ + let usernameCheck = username.toLowerCase() + const emailCheck = email.toLowerCase() + + const existingUser = await db.user.findUnique({ + where: { + email: emailCheck + } + }) + + if (existingUser) { + throw new Error('email already exists') + } + + let isUnique = false; + while (!isUnique) { + const existingUserName = await db.user.findUnique({ + where: { + username: usernameCheck + } + }) + + if (existingUserName) { + usernameCheck = `${username}${Math.floor(Math.random() * 1000)}` + } else { + isUnique = true; + } + } + + const user = await db.user.create({ data: { - email, + name: username, + username: usernameCheck, + email: emailCheck, password: hashed } }) return NextResponse.json({ - user: { - email: user.email - } + usernameOrEmail: user.email }) } catch (err: any) { + if (err.message === 'email already exists') { + return new NextResponse(JSON.stringify({ + error: err.message + }), { status: 422 } + ) + } + return new NextResponse( JSON.stringify({ error: err.message - }), - { - status: 500 - } + }), { status: 500 } ) } } \ No newline at end of file diff --git a/app/api/user/[userid].ts b/app/api/user/[userid].ts deleted file mode 100644 index 5fa1e7c..0000000 --- a/app/api/user/[userid].ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { prisma } from "@/lib/db"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'GET') { - return res.status(405).end(); - } - - try { - const { userId } = req.query; - - if (!userId || typeof userId !== 'string') { - throw new Error('Invalid ID'); - } - - const existingUser = await prisma.user.findUnique({ - where: { - id : +userId - } - }); - - return res.status(200).json({ ...existingUser}); - } catch (error) { - console.log(error); - return res.status(400).end(); - } -}; \ No newline at end of file diff --git a/app/api/user/index.ts b/app/api/user/index.ts deleted file mode 100644 index abb35ff..0000000 --- a/app/api/user/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { prisma } from "@/lib/db"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'GET') { - return res.status(405).end(); - } - - try { - const users = await prisma.user.findMany({ - orderBy: { - createdAt: 'desc' - } - }); - - return res.status(200).json(users); - } catch(error) { - console.log(error); - return res.status(400).end(); - } -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index f53ce33..421584b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import './globals.css' import Providers from '@/components/react-query/provider' import SiteLoad from '@/components/site-loading' import { ThemeProvider } from '@/components/ui/theme-provider' +import { Toaster } from '@/components/ui/toaster' import { Suspense } from 'react' const inter = Inter({ subsets: ['latin'] }) @@ -26,6 +27,7 @@ export default function RootLayout({ <Suspense fallback={<SiteLoad />}> <Providers> {children} + <Toaster /> </Providers> </Suspense> </ThemeProvider> diff --git a/components/auth-login-form.tsx b/components/auth-login-form.tsx deleted file mode 100644 index 0b430e2..0000000 --- a/components/auth-login-form.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import { Alert } from '@/components/ui/alert' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { signIn } from 'next-auth/react' -import { useRouter, useSearchParams } from 'next/navigation' -import { useState } from 'react' - -export const LoginForm = () => { - const router = useRouter() - const searchParams = useSearchParams() - const callbackUrl = searchParams.get('callbackUrl') || '/home' - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - try { - const res = await signIn('credentials', { - redirect: false, - email, - password, - callbackUrl - }) - console.log('Res', res) - if (!res?.error) { - router.push(callbackUrl) - } else { - setError('Invalid email or password') - } - } catch (err: any) { } - } - - return ( - <form onSubmit={onSubmit} className="space-y-12 w-full sm:w-[400px]"> - <div className="grid w-full items-center gap-1.5"> - <Label htmlFor="email">Email</Label> - <Input - className="w-full" - required - value={email} - onChange={(e) => setEmail(e.target.value)} - id="email" - type="email" - /> - </div> - <div className="grid w-full items-center gap-1.5"> - <Label htmlFor="password">Password</Label> - <Input - className="w-full" - required - value={password} - onChange={(e) => setPassword(e.target.value)} - id="password" - type="password" - /> - </div> - {error && <Alert>{error}</Alert>} - <div className="w-full"> - <Button className="w-full" size="lg"> - Login - </Button> - </div> - </form> - ) -} \ No newline at end of file diff --git a/components/auth-signup-form.tsx b/components/auth-signup-form.tsx index 1c1cb16..1d95dcd 100644 --- a/components/auth-signup-form.tsx +++ b/components/auth-signup-form.tsx @@ -5,13 +5,11 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useState } from 'react' -import { useRouter } from 'next/navigation' export const SignupForm = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState<string | null>(null) - const router = useRouter(); const onSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -28,7 +26,6 @@ export const SignupForm = () => { } }) if (res.ok) { - router.push("/login") } else { setError((await res.json()).error) } diff --git a/components/icons.tsx b/components/icons.tsx index e9a8c6b..6704d78 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -11,6 +11,7 @@ import { File, FileText, Gamepad2, + Github, Heart, HelpCircle, Home, @@ -71,12 +72,13 @@ export const Icons: IconsType = { help: HelpCircle, // Help Nav sun: SunMedium, // Light Mode Toggle Nav moon: Moon, // Dark Mode Toggle Nav + arrowupline: ArrowUpToLine, // Back to Top Button with line arrowdown: ArrowDown, // Descending Sort heart: Heart, // Like Button - arrowupline: ArrowUpToLine, // Back to Top Button + chevronLeft: ChevronLeft, // Back Login Arrow + spinner: Loader2, // Loading Spinner + github: Github, // Github Icon close: X, - spinner: Loader2, - chevronLeft: ChevronLeft, chevronRight: ChevronRight, trash: Trash, post: FileText, diff --git a/components/logo.tsx b/components/logo.tsx new file mode 100644 index 0000000..145e3c7 --- /dev/null +++ b/components/logo.tsx @@ -0,0 +1,10 @@ +import { Icons } from "./icons"; + +export function GameUnityLogo({ className }: { className?: string }) { + return ( + <> + <Icons.logo className={`dark:hidden ${className}`} /> + <Icons.logoWhite className={`hidden dark:block ${className}`} /> + </> + ) +} diff --git a/components/nav.tsx b/components/nav.tsx index 45b8874..e4743b8 100644 --- a/components/nav.tsx +++ b/components/nav.tsx @@ -4,10 +4,11 @@ import { Icons, IconsType } from "@/components/icons"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { SidebarNavItem } from "@/types"; +import { signIn, signOut, useSession } from "next-auth/react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { GameUnityLogo } from "./logo"; import { ModeToggle } from "./mode-toggle"; -import {signIn, signOut, useSession } from "next-auth/react" interface DashboardNavProps { items: SidebarNavItem[] @@ -19,7 +20,7 @@ export default function DashboardNav({ items }: DashboardNavProps) { if (!items?.length) { return null } - + const isLoaded = true const user = "test" @@ -27,8 +28,7 @@ export default function DashboardNav({ items }: DashboardNavProps) { <nav className="grid items-start gap-2"> <div className="flex items-center"> <Link href="/" className={cn("rounded-full p-3 hover:bg-accent")}> - <Icons.logo className="h-7 w-7 dark:hidden" /> - <Icons.logoWhite className="h-7 w-7 hidden dark:block" /> + <GameUnityLogo className="h-8 w-8" /> </Link> </div> {session?.user && isLoaded && user ? @@ -64,14 +64,14 @@ export default function DashboardNav({ items }: DashboardNavProps) { </div> } {session?.user && - <> - <p className="text-sky-600"> {session?.user.name}</p> - <button className=" text-red-500" onClick={() => signOut()}> - Sign Out - </button> - </> + <> + <p className="text-sky-600"> {session?.user.name}</p> + <button className=" text-red-500" onClick={() => signOut()}> + Sign Out + </button> + </> } - <ModeToggle /> + <ModeToggle /> </nav> ) } \ No newline at end of file diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx index 2deb2e9..f820588 100644 --- a/components/ui/toast.tsx +++ b/components/ui/toast.tsx @@ -1,6 +1,6 @@ import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast" -import { VariantProps, cva } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority" import { X } from "lucide-react" import { cn } from "@/lib/utils" diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000..ac9370c --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + <ToastProvider> + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + <Toast key={id} {...props}> + <div className="grid gap-1"> + {title && <ToastTitle>{title}</ToastTitle>} + {description && ( + <ToastDescription>{description}</ToastDescription> + )} + </div> + {action} + <ToastClose /> + </Toast> + ) + })} + <ToastViewport /> + </ToastProvider> + ) +} \ No newline at end of file diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts index c70c0d6..2c94c2d 100644 --- a/components/ui/use-toast.ts +++ b/components/ui/use-toast.ts @@ -1,7 +1,7 @@ // Inspired by react-hot-toast library import * as React from "react" -import { ToastActionElement, type ToastProps } from "@/components/ui/toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/toast" const TOAST_LIMIT = 1 const TOAST_REMOVE_DELAY = 1000000 @@ -135,7 +135,7 @@ function dispatch(action: Action) { }) } -interface Toast extends Omit<ToasterToast, "id"> {} +type Toast = Omit<ToasterToast, "id"> function toast({ ...props }: Toast) { const id = genId() diff --git a/components/user-auth-form.tsx b/components/user-auth-form.tsx new file mode 100644 index 0000000..f4f2072 --- /dev/null +++ b/components/user-auth-form.tsx @@ -0,0 +1,224 @@ +'use client' + +import { zodResolver } from "@hookform/resolvers/zod" +import { signIn } from 'next-auth/react' +import { useSearchParams } from "next/navigation" +import { HTMLAttributes, useState } from 'react' +import { useForm } from 'react-hook-form' +import * as z from "zod" + +import { Icons } from '@/components/icons' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ToastAction } from "@/components/ui/toast" +import { toast } from "@/components/ui/use-toast" +import { cn } from '@/lib/utils' +import { userAuthSchema } from "@/lib/validations/auth" + +interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> { + type: "login" | "signup" +} + +type FormData = z.infer<typeof userAuthSchema> + +export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm<FormData>({ + resolver: zodResolver(userAuthSchema), + }) + const [isLoading, setIsLoading] = useState<boolean>(false) + const [isGitHubLoading, setIsGitHubLoading] = useState<boolean>(false) + const searchParams = useSearchParams() + + async function onSubmit(data: FormData) { + setIsLoading(true) + + if (type === "signup") { + const res = await fetch('/api/signup', { + method: 'POST', + body: JSON.stringify({ + username: data.username, + email: data.email, + password: data.password + }), + headers: { + 'Content-Type': 'application/json' + } + }) + + if (!res.ok) { + if (res.status === 422) { + setError('email', { type: 'manual', message: 'This email is already in use. Please choose another one.' }); + } + + setIsLoading(false) + return toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "Your sign up request failed. Please try again.", + }) + } + } + + const signInResult = await signIn("credentials", { + usernameOrEmail: data.email?.toLowerCase() || data.usernameOrEmail?.toLowerCase(), + password: data.password, + redirect: true, + callbackUrl: searchParams?.get("from") || "/home", + }); + + setIsLoading(false) + + if (signInResult?.error) { + return toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "Your log in request failed. Please try again.", + action: <ToastAction altText="Try again">Try again</ToastAction>, + }) + } + + if (type === "signup") { + return toast({ + title: "Congratulations!", + description: "Your account has been created. You will be redirected shortly.", + }) + } else { + return toast({ + title: "Logging in.", + description: "You will be redirected shortly.", + }) + } + } + + return ( + <div className={cn("grid gap-6", className)} {...props}> + <form onSubmit={handleSubmit(onSubmit)}> + <div className="grid gap-2"> + {type === "login" ? + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="usernameOrEmail"> + Username or email + </Label> + <Input + id="usernameOrEmail" + placeholder="Your username or email" + type="text" + autoCapitalize="none" + autoComplete="username email" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("usernameOrEmail", { required: true })} + /> + {errors?.usernameOrEmail && ( + <p className="px-1 text-xs text-red-600"> + {errors.usernameOrEmail.message} + </p> + )} + </div> : null} + {type === "signup" ? + <> + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="username"> + Username + </Label> + <Input + id="username" + placeholder="Your username" + type="username" + autoCapitalize="none" + autoComplete="username" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("username", { required: true })} + /> + {errors?.username && ( + <p className="px-1 text-xs text-red-600"> + {errors.username.message} + </p> + )} + </div> + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="email"> + Email + </Label> + <Input + id="email" + placeholder="Your email" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("email", { required: true })} + /> + {errors?.email && ( + <p className="px-1 text-xs text-red-600"> + {errors.email.message} + </p> + )} + </div> + </> : null} + <div className="grid gap-1"> + <Label className="sr-only" htmlFor="password"> + Password + </Label> + <Input + id="password" + placeholder="Your password" + type="password" + autoCapitalize="none" + autoComplete="new-password" + autoCorrect="off" + disabled={isLoading || isGitHubLoading} + {...register("password", { required: true })} + /> + {errors?.password && ( + <p className="px-1 text-xs text-red-600"> + {errors.password.message} + </p> + )} + </div> + <Button disabled={isLoading} type="submit"> + {isLoading && ( + <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> + )} + {type === "signup" ? "Sign Up" : "Log In"} + </Button> + </div> + </form> + + <div className="relative"> + <div className="absolute inset-0 flex items-center"> + <span className="w-full border-t" /> + </div> + <div className="relative flex justify-center text-xs uppercase"> + <span className="bg-background px-2 text-muted-foreground"> + Or continue with + </span> + </div> + </div> + <Button + variant="outline" + type="button" + onClick={() => { + setIsGitHubLoading(true) + signIn("github", { callbackUrl: searchParams?.get("from") || "/home" }) + }} + disabled={isLoading || isGitHubLoading} + > + {isGitHubLoading ? ( + <Icons.spinner className="mr-2 h-4 w-4 animate-spin" /> + ) : ( + <Icons.github className="mr-2 h-4 w-4" /> + )}{" "} + Github + </Button> + </div> + ) +} \ No newline at end of file diff --git a/components/user-item.tsx b/components/user-item.tsx index 0da145d..64c729d 100644 --- a/components/user-item.tsx +++ b/components/user-item.tsx @@ -3,19 +3,19 @@ import Link from "next/link"; import FollowButton from "./following-button"; // this is a single user helper-component, only for design purposes -export default function FollowUser({ id, followId, userName, image }: { id: number, followId: number, userName: string, image: { url: string } }) { +export default function FollowUser({ id, followId, username, image }: { id: number, followId: number, username: string, image: { url: string } }) { return ( <div> <Link href={`/user/${id}`}> <div className=""> <Image src={image.url} - alt={userName} + alt={username} width={50} height={50} priority={true} /> </div> - <p>{userName}</p> + <p>{username}</p> <FollowButton userId={id} followerId={followId} /> </Link> </div> diff --git a/env.mjs b/env.mjs new file mode 100644 index 0000000..0154d5d --- /dev/null +++ b/env.mjs @@ -0,0 +1,33 @@ +import { createEnv } from "@t3-oss/env-nextjs" +import { z } from "zod" + +export const env = createEnv({ + server: { + DATABASE_URL: z.string().min(1), + GITHUB_CLIENT_ID: z.string().min(1), + GITHUB_CLIENT_SECRET: z.string().min(1), + NEXTAUTH_URL: z.string().url().optional(), + NEXTAUTH_SECRET: z.string().min(1), + TWITCH_CLIENT_ID: z.string().min(1), + TWITCH_CLIENT_SECRET: z.string().min(1), + TWITCH_AUTH_BASE_URL: z.string().url().optional(), + IGDB_BASE_URL: z.string().url().optional(), + IGDB_IMG_BASE_URL: z.string().url().optional(), + }, + client: { + NEXT_PUBLIC_APP_URL: z.string().min(1), + }, + runtimeEnv: { + DATABASE_URL: process.env.DATABASE_URL, + GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, + NEXTAUTH_URL: process.env.NEXTAUTH_URL, + NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, + NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, + TWITCH_CLIENT_ID: process.env.TWITCH_CLIENT_ID, + TWITCH_CLIENT_SECRET: process.env.TWITCH_CLIENT_SECRET, + TWITCH_AUTH_BASE_URL: process.env.TWITCH_AUTH_BASE_URL, + IGDB_BASE_URL: process.env.IGDB_BASE_URL, + IGDB_IMG_BASE_URL: process.env.IGDB_IMG_BASE_URL, + }, +}) \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..7763e34 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,99 @@ +import { env } from "@/env.mjs" +import { db } from "@/lib/db" +import { PrismaAdapter } from "@auth/prisma-adapter" +import { compare } from "bcrypt" +import { NextAuthOptions } from "next-auth" +import { Adapter } from "next-auth/adapters" +import CredentialsProvider from 'next-auth/providers/credentials' +import GitHubProvider from "next-auth/providers/github" + +export const authOptions: NextAuthOptions = { + adapter: PrismaAdapter(db as any) as Adapter, + session: { + strategy: 'jwt' + }, + pages: { + signIn: "/login", + }, + providers: [ + GitHubProvider({ + clientId: env.GITHUB_CLIENT_ID, + clientSecret: env.GITHUB_CLIENT_SECRET, + }), + + CredentialsProvider({ + name: 'Login', + credentials: { + usernameOrEmail: { label: 'Username or Email', type: 'text' }, + password: { label: 'Password', type: 'password' } + }, + async authorize(credentials) { + if (!credentials?.usernameOrEmail || !credentials?.password) { + return null + } + + const user = await db.user.findFirst({ + where: { + OR: [ + { username: credentials.usernameOrEmail.toLowerCase() }, + { email: credentials.usernameOrEmail.toLowerCase() }, + ], + }, + }); + + if (!user || !user.password) { + return null + } + + const isPasswordValid = await compare( + credentials.password, + user.password + ) + + if (!isPasswordValid) { + return null + } + + return { + id: user.id, + username: user.username, + email: user.email, + } + } + }) + ], + secret: env.NEXTAUTH_SECRET, + callbacks: { + async session({ token, session }) { + if (token) { + session.user.id = token.id + session.user.name = token.name + session.user.email = token.email + session.user.image = token.picture + } + + return session + }, + async jwt({ token, user }) { + const dbUser = await db.user.findFirst({ + where: { + email: token.email, + }, + }) + + if (!dbUser) { + if (user) { + token.id = user?.id + } + return token + } + + return { + id: dbUser.id, + name: dbUser.name, + email: dbUser.email, + picture: dbUser.image, + } + } + } +} \ No newline at end of file diff --git a/lib/db.ts b/lib/db.ts index e67728c..8a33a38 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,13 +1,18 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from "@prisma/client" -const globalForPrisma = global as unknown as { - prisma: PrismaClient | undefined +declare global { + // eslint-disable-next-line no-var + var cachedPrisma: PrismaClient } -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - // log: ['query'], - }) +let prisma: PrismaClient +if (process.env.NODE_ENV === "production") { + prisma = new PrismaClient() +} else { + if (!global.cachedPrisma) { + global.cachedPrisma = new PrismaClient() + } + prisma = global.cachedPrisma +} -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma \ No newline at end of file +export const db = prisma \ No newline at end of file diff --git a/lib/igdb.ts b/lib/igdb.ts index d48e98e..93d0d43 100644 --- a/lib/igdb.ts +++ b/lib/igdb.ts @@ -1,11 +1,12 @@ +import { env } from "@/env.mjs" import { calculateOffset, getImageURL } from "@/lib/utils" import { IAuth, IGame } from "@/types/igdb-types" -const TWITCH_AUTH_BASE_URL = process.env.TWITCH_AUTH_BASE_URL ?? '' -const IGDB_BASE_URL = process.env.IGDB_BASE_URL ?? '' +const TWITCH_AUTH_BASE_URL = env.TWITCH_AUTH_BASE_URL ?? '' +const IGDB_BASE_URL = env.IGDB_BASE_URL ?? '' -const CLIENT_ID = process.env.TWITCH_CLIENT_ID ?? '' -const CLIENT_SECRET = process.env.TWITCH_CLIENT_SECRET ?? '' +const CLIENT_ID = env.TWITCH_CLIENT_ID ?? '' +const CLIENT_SECRET = env.TWITCH_CLIENT_SECRET ?? '' const limit = 100 diff --git a/lib/session.ts b/lib/session.ts new file mode 100644 index 0000000..1bc9e23 --- /dev/null +++ b/lib/session.ts @@ -0,0 +1,9 @@ +import { getServerSession } from "next-auth/next" + +import { authOptions } from "@/lib/auth" + +export async function getCurrentUser() { + const session = await getServerSession(authOptions) + + return session?.user +} \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts index 6a89161..3d7f8f6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,3 +1,4 @@ +import { env } from "@/env.mjs" import { ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" @@ -6,10 +7,9 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } -const IGDB_IMG_BASE_URL = process.env.IGDB_IMG_BASE_URL ?? '' - // changes the default size of the image to be fetched export function getImageURL(hashId: string, size: string): string { + const IGDB_IMG_BASE_URL = env.IGDB_IMG_BASE_URL ?? '' return `${IGDB_IMG_BASE_URL}/t_${size}/${hashId}.jpg` } diff --git a/lib/validations/auth.ts b/lib/validations/auth.ts index e65b5cb..e749cbd 100644 --- a/lib/validations/auth.ts +++ b/lib/validations/auth.ts @@ -1,5 +1,13 @@ -import * as z from "zod" +import * as z from "zod"; export const userAuthSchema = z.object({ - email: z.string().email(), -}) \ No newline at end of file + usernameOrEmail: z + .union([ + z.string().min(3, "Username or email must be at least 3 characters").max(15, "Username or email must be at most 15 characters"), + z.string().email("Invalid email format"), + ]) + .optional(), + username: z.string().min(3, "Username must be at least 3 characters").max(15, "Username must be at most 15 characters").optional(), + email: z.string().email("Invalid email format").optional(), + password: z.string().min(6, "Password must be at least 6 characters").max(18, "Password must be at most 18 characters"), +}); \ No newline at end of file diff --git a/next.config.js b/next.config.mjs similarity index 61% rename from next.config.js rename to next.config.mjs index ce446f9..be988d3 100644 --- a/next.config.js +++ b/next.config.mjs @@ -1,9 +1,13 @@ +import "./env.mjs" + /** @type {import('next').NextConfig} */ const nextConfig = { + reactStrictMode: true, + swcMinify: true, images: { unoptimized: true, domains: ["images.igdb.com"] } } -module.exports = nextConfig +export default nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d7fdf5d..f3aa7f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "project_ss23_gameunity", "version": "0.2.0", "dependencies": { + "@auth/prisma-adapter": "^1.0.0", + "@hookform/resolvers": "^3.1.0", "@prisma/client": "^4.15.0", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", @@ -15,33 +17,36 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", + "@t3-oss/env-nextjs": "^0.4.0", "@tanstack/react-query": "^4.29.12", "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", - "lucide-react": "^0.224.0", + "lucide-react": "^0.234.0", "next": "^13.4.4", "next-auth": "^4.22.1", "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.44.3", "react-infinite-scroll-component": "^6.1.0", - "tailwind-merge": "^1.12.0", - "tailwindcss-animate": "^1.0.5" + "tailwind-merge": "^1.13.0", + "tailwindcss-animate": "^1.0.5", + "zod": "^3.21.4" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^4.29.9", "@types/bcrypt": "^5.0.0", "@types/node": "^20.2.5", - "@types/react": "^18.2.7", + "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", "autoprefixer": "10.4.14", - "eslint": "^8.41.0", + "eslint": "^8.42.0", "eslint-config-next": "^13.4.4", "postcss": "8.4.24", "prisma": "^4.15.0", "tailwindcss": "3.3.2", - "typescript": "^5.0.4" + "typescript": "^5.1.3" } }, "node_modules/@alloc/quick-lru": { @@ -55,6 +60,58 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.8.1.tgz", + "integrity": "sha512-WudBmZudZ/cvykxHV5hIwrYsd7AlETQ535O7w3sSiiumT28+U9GvBb8oSRtfzxpW9rym3lAdfeTJqGA8U4FecQ==", + "dependencies": { + "@panva/hkdf": "^1.0.4", + "cookie": "0.5.0", + "jose": "^4.11.1", + "oauth4webapi": "^2.0.6", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/core/node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/core/node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.0.tgz", + "integrity": "sha512-+x+s5dgpNmqrcQC2ZRAXZIM6yhkWP/EXjIUgqUyMepLiX1OHi2AXIUAAbXsW4oG9OpYr/rvPIzPBpuGt6sPFwQ==", + "dependencies": { + "@auth/core": "0.8.1" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4" + } + }, "node_modules/@babel/runtime": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", @@ -114,9 +171,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", - "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz", + "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -147,10 +204,18 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.0.tgz", + "integrity": "sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -1210,6 +1275,27 @@ "tslib": "^2.4.0" } }, + "node_modules/@t3-oss/env-core": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@t3-oss/env-core/-/env-core-0.4.0.tgz", + "integrity": "sha512-6JlMp0Vru15q/axHzBKsQQjiyGS6k+EsZBY1iErGVmOGzNSoVluBahnYFP7tEkwZ7KoRgSq4NRIc1Ez7SVYuxQ==", + "peerDependencies": { + "typescript": ">=4.7.2", + "zod": "^3.0.0" + } + }, + "node_modules/@t3-oss/env-nextjs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@t3-oss/env-nextjs/-/env-nextjs-0.4.0.tgz", + "integrity": "sha512-K1u2i+S/uEhjfg++FqWlOzS6x237EARRbWGowH2MkDkFu2q7ZJSiJBJT8e47L7NHWH5IyZrTCM6BdOxyWEnQuQ==", + "dependencies": { + "@t3-oss/env-core": "0.4.0" + }, + "peerDependencies": { + "typescript": ">=4.7.2", + "zod": "^3.0.0" + } + }, "node_modules/@tanstack/eslint-plugin-query": { "version": "4.29.9", "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-4.29.9.tgz", @@ -1283,9 +1369,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.7.tgz", - "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "version": "18.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz", + "integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -2381,16 +2467,16 @@ } }, "node_modules/eslint": { - "version": "8.41.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", - "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz", + "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint/js": "8.42.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", @@ -3961,9 +4047,9 @@ } }, "node_modules/lucide-react": { - "version": "0.224.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.224.0.tgz", - "integrity": "sha512-2QuPhbEAicN1Ak9DSeViYhZLYogW54e802IFDasoy/AXKGrnzTcBLQM8gidXbOd2DSWbrLHDgN6n4340QzyujQ==", + "version": "0.234.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.234.0.tgz", + "integrity": "sha512-7MtwK9zPXyvpHv9Tf0anraIjI9yRDdkfYIPt8KOC54NvimBAxbNpeIBb5/f+tZVIkN0hMgZdrAFugZe4YPZHcA==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -4341,6 +4427,14 @@ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" }, + "node_modules/oauth4webapi": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.3.0.tgz", + "integrity": "sha512-JGkb5doGrwzVDuHwgrR4nHJayzN4h59VCed6EW8Tql6iHDfZIabCJvg6wtbn5q6pyB2hZruI3b77Nudvq7NmvA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4909,6 +5003,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.44.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.44.3.tgz", + "integrity": "sha512-/tHId6p2ViAka1wECMw8FEPn/oz/w226zehHrJyQ1oIzCBNMIJCaj6ZkQcv+MjDxYh9MWR7RQic7Qqwe4a5nkw==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-infinite-scroll-component": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", @@ -5594,9 +5703,9 @@ } }, "node_modules/tailwind-merge": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.12.0.tgz", - "integrity": "sha512-Y17eDp7FtN1+JJ4OY0Bqv9OA41O+MS8c1Iyr3T6JFLnOgLg3EvcyMKZAnQ8AGyvB5Nxm3t9Xb5Mhe139m8QT/g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.13.0.tgz", + "integrity": "sha512-mUTmDbcU+IhOvJ0c42eLQ/nRkvolTqfpVaVQRSxfJAv9TabS6Y2zW/1wKpKLdKzyL3Gh8j6NTLl6MWNmvOM6kA==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -5815,16 +5924,15 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "devOptional": true, + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { diff --git a/package.json b/package.json index 3c968f4..926c0c1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "preview": "next build && next start" }, "dependencies": { + "@auth/prisma-adapter": "^1.0.0", + "@hookform/resolvers": "^3.1.0", "@prisma/client": "^4.15.0", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", @@ -18,32 +20,35 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", + "@t3-oss/env-nextjs": "^0.4.0", "@tanstack/react-query": "^4.29.12", "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", - "lucide-react": "^0.224.0", + "lucide-react": "^0.234.0", "next": "^13.4.4", "next-auth": "^4.22.1", "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.44.3", "react-infinite-scroll-component": "^6.1.0", - "tailwind-merge": "^1.12.0", - "tailwindcss-animate": "^1.0.5" + "tailwind-merge": "^1.13.0", + "tailwindcss-animate": "^1.0.5", + "zod": "^3.21.4" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^4.29.9", "@types/bcrypt": "^5.0.0", "@types/node": "^20.2.5", - "@types/react": "^18.2.7", + "@types/react": "^18.2.8", "@types/react-dom": "^18.2.4", "autoprefixer": "10.4.14", - "eslint": "^8.41.0", + "eslint": "^8.42.0", "eslint-config-next": "^13.4.4", "postcss": "8.4.24", "prisma": "^4.15.0", "tailwindcss": "3.3.2", - "typescript": "^5.0.4" + "typescript": "^5.1.3" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dbf586a..c9a096a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,15 +10,53 @@ datasource db { url = env("DATABASE_URL") } +model Account { + id String @id @default(cuid()) + userId String @unique + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + refresh_token_expires_in Int? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@map(name: "accounts") +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String @unique + expires DateTime + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map(name: "sessions") +} + model User { - id Int @id @default(autoincrement()) - userName String? @unique - name String? @default("u ${id}") + id String @id @default(cuid()) + name String? + username String? @unique @map("usernames") email String? @unique - password String emailVerified DateTime? - image String? - createdAt DateTime @default(now()) + password String? + image String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + + accounts Account[] + sessions Session[] Post Post[] Comment Comment[] @@ -26,48 +64,71 @@ model User { followers Follows[] @relation("follower") following Follows[] @relation("following") + + @@map(name: "users") +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map(name: "verification_tokens") } model Follows { follower User @relation("following", fields: [followerId], references: [id]) - followerId Int + followerId String following User @relation("follower", fields: [followingId], references: [id]) - followingId Int + followingId String createdAt DateTime @default(now()) @@id([followerId, followingId]) + @@map(name: "follows") } model Post { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") + userId String content String likeCount Int? @default(0) - gameId Int? published Boolean @default(false) - userId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) Comment Comment[] Like Like[] + + @@map(name: "posts") } model Like { - id Int @id @default(autoincrement()) - postId Int - userId Int + id String @id @default(cuid()) + postId String + commentId String? + userId String - post Post @relation(fields: [postId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map(name: "likes") } model Comment { - id Int @id @default(autoincrement()) + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @map(name: "updated_at") message String - postId Int - userId Int - createdAt DateTime @default(now()) - post Post @relation(fields: [postId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + likeCount Int? @default(0) + postId String + userId String + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + Like Like[] + + @@map(name: "comments") } diff --git a/tsconfig.json b/tsconfig.json index a914549..88c4dba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "@/*": [ "./*" ] - } + }, + "strictNullChecks": true }, "include": [ "next-env.d.ts", diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 007c24c..19ff225 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -1,12 +1,18 @@ -import 'next-auth'; +import { User } from "next-auth" +import { JWT } from "next-auth/jwt" -declare module 'next-auth' { +type UserId = string + +declare module "next-auth/jwt" { + interface JWT { + id: UserId + } +} + +declare module "next-auth" { interface Session { - user: { - id: string; - name?: string | null; - email?: string | null; - image?: string | null; - }; + user: User & { + id: UserId + } } } \ No newline at end of file -- GitLab