From d032c0c720501829b73a42c34b01c982619f6d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Akg=C3=BCl?= <s86116@bht-berlin.de> Date: Sun, 4 Jun 2023 05:19:58 +0200 Subject: [PATCH] beg auth change --- 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 | 1 + app/api/likes/likeService.ts | 12 +- app/api/messages/route.ts | 10 +- app/api/signup/route.ts | 22 ++-- app/api/user/[userid].ts | 28 ---- app/api/user/index.ts | 22 ---- components/auth-login-form.tsx | 69 ---------- components/auth-signup-form.tsx | 71 ---------- components/icons.tsx | 8 +- components/logo.tsx | 10 ++ components/nav.tsx | 22 ++-- components/user-auth-form.tsx | 193 ++++++++++++++++++++++++++++ components/user-item.tsx | 6 +- env.mjs | 33 +++++ lib/auth.ts | 89 +++++++++---- lib/db.ts | 23 ++-- lib/igdb.ts | 9 +- lib/utils.ts | 4 +- lib/validations/auth.ts | 2 + next.config.js => next.config.mjs | 6 +- package-lock.json | 112 +++++++++++++++- package.json | 7 +- prisma/schema.prisma | 107 +++++++++++---- tsconfig.json | 3 +- 28 files changed, 667 insertions(+), 330 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 delete mode 100644 components/auth-signup-form.tsx create mode 100644 components/logo.tsx create mode 100644 components/user-auth-form.tsx create mode 100644 env.mjs rename next.config.js => next.config.mjs (61%) 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 dbc44a8..17d6a45 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -4,3 +4,4 @@ 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 56ccc74..6abf1de 100644 --- a/app/api/messages/route.ts +++ b/app/api/messages/route.ts @@ -1,5 +1,5 @@ import { authOptions } from "@/lib/auth"; -import { prisma } from "@/lib/db"; +import { db } from "@/lib/db"; import { Prisma } from "@prisma/client"; import { getServerSession } from "next-auth/next"; import { revalidatePath } from "next/cache"; @@ -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: { 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") @@ -49,7 +49,7 @@ export async function GET(req: NextRequest, res: NextResponse) { } try { - const messages = await prisma.post.findMany({ + const messages = await db.post.findMany({ orderBy: { createdAt: "desc" } diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts index 79937ac..da96437 100644 --- a/app/api/signup/route.ts +++ b/app/api/signup/route.ts @@ -1,32 +1,28 @@ -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({ + const user = await db.user.create({ data: { + username, email, password: hashed } }) return NextResponse.json({ - user: { - email: user.email - } + username: user.username, + email: user.email }) } catch (err: any) { - return new NextResponse( - JSON.stringify({ - error: err.message - }), - { - status: 500 - } + return new NextResponse(JSON.stringify({ + error: err.message + }), { 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 6a4bbd0..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 851c256..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/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 deleted file mode 100644 index 4eb7e63..0000000 --- a/components/auth-signup-form.tsx +++ /dev/null @@ -1,71 +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 { useState } from 'react' - -export const SignupForm = () => { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState<string | null>(null) - - const onSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - try { - const res = await fetch('/api/signup', { - method: 'POST', - body: JSON.stringify({ - email, - password - }), - headers: { - 'Content-Type': 'application/json' - } - }) - if (res.ok) { - signIn() - } else { - setError((await res.json()).error) - } - } catch (error: any) { - setError(error?.message) - } - } - - 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"> - Sign up - </Button> - </div> - </form> - ) -} \ No newline at end of file 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/user-auth-form.tsx b/components/user-auth-form.tsx new file mode 100644 index 0000000..9820124 --- /dev/null +++ b/components/user-auth-form.tsx @@ -0,0 +1,193 @@ +'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 { useToast } from '@/components/ui/use-toast' +import { cn } from '@/lib/utils' +import { userAuthSchema } from '@/lib/validations/auth' +import { ToastAction } from "./ui/toast" + +interface UserAuthFormProps extends HTMLAttributes<HTMLDivElement> { + type: "login" | "signup" +} + +type FormData = z.infer<typeof userAuthSchema> + +export function UserAuthForm({ type, className, ...props }: UserAuthFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<FormData>({ + resolver: zodResolver(userAuthSchema), + }) + const [isLoading, setIsLoading] = useState<boolean>(false) + const [isGitHubLoading, setIsGitHubLoading] = useState<boolean>(false) + const searchParams = useSearchParams() + + const { toast } = useToast() + + 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) { + setIsLoading(false) + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "Your sign up request failed. Please try again.", + }) + } + } + + const signInResult = await signIn("credentials", { + username: data.username, + email: data.email, + password: data.password, + redirect: false, + callbackUrl: "/home", + }); + + setIsLoading(false) + + if (!signInResult?.ok) { + 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>, + }) + } + + // toast({ + // title: "Check your email.", + // description: "We sent you a login link. Be sure to check your spam too.", + // }) + } + + return ( + <> + <div className={cn("grid gap-6", className)} {...props}> + <form onSubmit={handleSubmit(onSubmit)}> + <div className="grid gap-2"> + <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")} + /> + {errors?.username && ( + <p className="px-1 text-xs text-red-600"> + {errors.username.message} + </p> + )} + </div> + {type === "signup" ? <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")} + /> + {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")} + /> + {errors?.password && ( + <p className="px-1 text-xs text-red-600"> + {errors.password.message} + </p> + )} + </div> + <Button disabled={isLoading}> + {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") + }} + 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 index 2a10ac9..54f6453 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,29 +1,54 @@ +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 { prisma } from "./db" +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 as string, + clientSecret: env.GITHUB_CLIENT_SECRET as string, + }), + CredentialsProvider({ - name: 'Sign in', + name: 'Login', credentials: { - email: { - label: 'Email', - type: 'email', - placeholder: 'hello@example.com' - }, + username: { label: 'Username', type: 'text' }, + email: { label: 'Email', type: 'email', placeholder: 'hello@example.com' }, password: { label: 'Password', type: 'password' } }, async authorize(credentials) { - if (!credentials?.email || !credentials.password) { + if (!credentials?.username || !credentials.email || !credentials.password) { return null } - const user = await prisma.user.findUnique({ + let isUnique = false; + while (!isUnique) { + const existingUserName = await db.user.findUnique({ + where: { + username: credentials.username + } + }) + + if (existingUserName) { + credentials.username = `${credentials.username}${Math.floor(Math.random() * 1000)}` + } else { + isUnique = true; + } + } + + const user = await db.user.findUnique({ where: { email: credentials.email } @@ -43,34 +68,46 @@ export const authOptions: NextAuthOptions = { } return { - id: user.id + '', + id: user.id, + username: user.username, email: user.email, name: user.name, } } }) ], + secret: env.NEXTAUTH_SECRET, callbacks: { - session: ({ session, token }) => { - console.log('Session Callback', { session, token }) - return { - ...session, - user: { - ...session.user, - id: token.id, - } + 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 }, - jwt: ({ token, user }) => { - console.log('JWT Callback', { token, user }) - if (user) { - const u = user as unknown as any - return { - ...token, - id: u.id, + 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, } - return token } } } \ 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/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..68de27a 100644 --- a/lib/validations/auth.ts +++ b/lib/validations/auth.ts @@ -1,5 +1,7 @@ import * as z from "zod" export const userAuthSchema = z.object({ + username: z.string().min(3).max(15), email: z.string().email(), + password: z.string().min(6).max(18), }) \ 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..1e17dcd 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,6 +17,7 @@ "@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", @@ -25,9 +28,11 @@ "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" + "tailwindcss-animate": "^1.0.5", + "zod": "^3.21.4" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^4.29.9", @@ -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", @@ -147,6 +204,14 @@ "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", @@ -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", @@ -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", @@ -5818,7 +5927,6 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 3c968f4..5dc490f 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,6 +20,7 @@ "@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", @@ -28,9 +31,11 @@ "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" + "tailwindcss-animate": "^1.0.5", + "zod": "^3.21.4" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^4.29.9", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dbf586a..70b33ff 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 + 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 + 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("username") 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[] @@ -28,46 +66,67 @@ model User { following Follows[] @relation("following") } +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", -- GitLab