From 5a1c2340cadddc0266237cdd17424272f1e3a1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Akg=C3=BCl?= <s86116@bht-berlin.de> Date: Tue, 6 Jun 2023 02:58:55 +0200 Subject: [PATCH] small auth additions and some nav fixes --- app/(content)/layout.tsx | 3 ++ app/api/signup/route.ts | 6 ++- app/globals.css | 16 ++++++- components/icons.tsx | 20 ++++----- components/mode-toggle.tsx | 14 +++--- components/nav.tsx | 86 ++++++++++++++++++------------------- components/search-input.tsx | 2 +- components/site-footer.tsx | 38 ++++++++-------- components/ui/button.tsx | 3 +- lib/auth.ts | 36 ++++++++++++++++ package-lock.json | 24 +++++++++-- package.json | 5 ++- prisma/schema.prisma | 2 +- types/next-auth.d.ts | 7 ++- 14 files changed, 168 insertions(+), 94 deletions(-) diff --git a/app/(content)/layout.tsx b/app/(content)/layout.tsx index 2d8363e..8a44982 100644 --- a/app/(content)/layout.tsx +++ b/app/(content)/layout.tsx @@ -1,6 +1,7 @@ import DashboardNav from "@/components/nav" import { SiteFooter } from "@/components/site-footer" import { dashboardConfig } from "@/lib/config/dashboard" +import { getCurrentUser } from "@/lib/session" interface DashboardLayoutProps { children?: React.ReactNode @@ -8,6 +9,8 @@ interface DashboardLayoutProps { export default async function ContentLayout({ children, }: DashboardLayoutProps) { + const user = await getCurrentUser() + return ( <div className="flex flex-col min-h-screen"> <div className="mx-32 my-6 flex-1 md:grid md:grid-cols-[220px_1fr] md:gap-6 lg:grid-cols-[240px_1fr] lg:gap-10"> diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts index 124f2fd..6d2b390 100644 --- a/app/api/signup/route.ts +++ b/app/api/signup/route.ts @@ -1,13 +1,15 @@ import { db } from '@/lib/db' import { hash } from 'bcrypt' import { NextResponse } from 'next/server' +import { normalize } from 'normalize-diacritics' export async function POST(req: Request) { try { const { username, email, password } = await req.json() const hashed = await hash(password, 12) - let usernameCheck = username.toLowerCase() + const normalizedName = await normalize(username.toLowerCase()); + let usernameCheck = normalizedName const emailCheck = email.toLowerCase() const existingUser = await db.user.findUnique({ @@ -29,7 +31,7 @@ export async function POST(req: Request) { }) if (existingUserName) { - usernameCheck = `${username}${Math.floor(Math.random() * 1000)}` + usernameCheck = `${normalizedName}${Math.floor(Math.random() * 1000)}` } else { isUnique = true; } diff --git a/app/globals.css b/app/globals.css index 6ae9916..af41b8b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,38 +4,52 @@ @layer base { :root { + /* Default background color of <body />...etc */ --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; + /* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */ --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; + /* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */ --popover: 0 0% 100%; --popover-foreground: 222.2 47.4% 11.2%; + /* Background color for <Card /> */ --card: 0 0% 100%; --card-foreground: 222.2 47.4% 11.2%; + /* Default border color */ --border: 214.3 31.8% 91.4%; + + /* Border color for inputs such as <Input />, <Select />, <Textarea /> */ --input: 214.3 31.8% 91.4%; - --primary: 222.2 47.4% 11.2%; + /* Primary colors for <Button /> */ + --primary: 255 83% 65%; --primary-foreground: 210 40% 98%; + /* Secondary colors for <Button /> */ --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; + /* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */ --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; + /* Used for destructive actions such as <Button variant="destructive"> */ --destructive: 0 100% 50%; --destructive-foreground: 210 40% 98%; + /* Used for focus ring */ --ring: 215 20.2% 65.1%; + /* Border radius for card, input and buttons */ --radius: 0.5rem; } + /* Same for Dark-Mode */ .dark { --background: 224 71% 4%; --foreground: 213 31% 91%; diff --git a/components/icons.tsx b/components/icons.tsx index 6704d78..cdacdf5 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -41,18 +41,18 @@ export type Icon = LucideIcon export const Icons: IconsType = { logo: ({ ...props }: LucideProps) => ( - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> - <path d="M13.7067 17.1466V22C13.7067 23.1046 12.8113 24 11.7067 24H8.85339C7.74882 24 6.85339 23.1046 6.85339 22V17.1466L10.3417 13.6667L13.7067 17.1466Z" fill="#16161A" /> - <path d="M12.2799 0H15.1332C16.2378 0 17.1332 0.895431 17.1332 2V6.85339L13.6776 10.3333L10.2799 6.85339V2C10.2799 0.895429 11.1753 0 12.2799 0Z" fill="#16161A" /> - <path d="M2 6.86666H6.85332L10.3416 10.3333L6.85332 13.72H2C0.895432 13.72 0 12.8246 0 11.72V8.86666C0 7.76209 0.895431 6.86666 2 6.86666Z" fill="#7F5AF0" /> - <path d="M17.1466 10.2934H21.9999C23.1045 10.2934 23.9999 11.1888 23.9999 12.2934V15.1466C23.9999 16.2512 23.1045 17.1466 21.9999 17.1466H17.1466L13.6776 13.6667L17.1466 10.2934Z" fill="#16161A" /> + <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> + <path d="M20.5603 25.7199V34C20.5603 35.1046 19.6649 36 18.5603 36H12.2803C11.1757 36 10.2803 35.1046 10.2803 34V25.7199L15.5127 20.5L20.5603 25.7199Z" fill="#16161A" /> + <path d="M17.4199 0H23.6999C24.8045 0 25.6999 0.89543 25.6999 2V10.2801L20.5164 15.5L15.4199 10.2801V2C15.4199 0.895429 16.3154 0 17.4199 0Z" fill="#16161A" /> + <path d="M2 10.2988H10.28L15.5124 15.4988L10.28 20.5789H2C0.895428 20.5789 0 19.6835 0 18.5789V12.2988C0 11.1943 0.895431 10.2988 2 10.2988Z" fill="#7F5AF0" /> + <path d="M25.7201 15.4395H34.0001C35.1047 15.4395 36.0001 16.3349 36.0001 17.4395V23.7193C36.0001 24.8239 35.1047 25.7193 34.0001 25.7193H25.7201L20.5166 20.4994L25.7201 15.4395Z" fill="#16161A" /> </svg>), logoWhite: ({ ...props }: LucideProps) => ( - <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> - <path d="M13.7067 17.1466V22C13.7067 23.1046 12.8113 24 11.7067 24H8.85339C7.74882 24 6.85339 23.1046 6.85339 22V17.1466L10.3417 13.6667L13.7067 17.1466Z" fill="#FFFFFE" /> - <path d="M12.2799 0H15.1332C16.2378 0 17.1332 0.895431 17.1332 2V6.85339L13.6776 10.3333L10.2799 6.85339V2C10.2799 0.895429 11.1753 0 12.2799 0Z" fill="#FFFFFE" /> - <path d="M2 6.86666H6.85332L10.3416 10.3333L6.85332 13.72H2C0.895432 13.72 0 12.8246 0 11.72V8.86666C0 7.76209 0.895431 6.86666 2 6.86666Z" fill="#FFFFFE" /> - <path d="M17.1466 10.2934H21.9999C23.1045 10.2934 23.9999 11.1888 23.9999 12.2934V15.1466C23.9999 16.2512 23.1045 17.1466 21.9999 17.1466H17.1466L13.6776 13.6667L17.1466 10.2934Z" fill="#FFFFFE" /> + <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> + <path d="M20.5602 25.7198V33.9998C20.5602 35.1044 19.6647 35.9998 18.5602 35.9998H12.2803C11.1757 35.9998 10.2803 35.1044 10.2803 33.9998V25.7198L15.5127 20.5L20.5602 25.7198Z" fill="#FFFFFE" /> + <path d="M17.4199 0H23.6998C24.8043 0 25.6998 0.895431 25.6998 2V10.2799L20.5164 15.4998L15.4199 10.2799V2C15.4199 0.895433 16.3154 0 17.4199 0Z" fill="#FFFFFE" /> + <path d="M2 10.2998H10.2799L15.5122 15.4997L10.2799 20.5798H2C0.895432 20.5798 0 19.6843 0 18.5798V12.2998C0 11.1952 0.895431 10.2998 2 10.2998Z" fill="#7F5AF0" /> + <path d="M25.72 15.4385H33.9999C35.1045 15.4385 35.9999 16.3339 35.9999 17.4385V23.7182C35.9999 24.8228 35.1045 25.7182 33.9999 25.7182H25.72L20.5166 20.4983L25.72 15.4385Z" fill="#FFFFFE" /> </svg>), logoName: ({ ...props }: LucideProps) => ( <svg width="114" height="22" viewBox="0 0 114 22" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> diff --git a/components/mode-toggle.tsx b/components/mode-toggle.tsx index 2220f93..aa1e582 100644 --- a/components/mode-toggle.tsx +++ b/components/mode-toggle.tsx @@ -8,6 +8,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { cn } from "@/lib/utils" import { useTheme } from "next-themes" export function ModeToggle() { @@ -16,13 +17,16 @@ export function ModeToggle() { return ( <DropdownMenu> <DropdownMenuTrigger asChild> - <Button variant="ghost" size="sm" className="h-8 w-8 px-0"> - <Icons.sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> - <Icons.moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> + <span className={cn( + "group flex items-center rounded-md px-3 py-2 font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer", + )}> + <Icons.sun className="mr-3 h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> + <Icons.moon className="mr-3 h-5 w-5 absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> - </Button> + <span>Mode</span> + </span> </DropdownMenuTrigger> - <DropdownMenuContent align="end"> + <DropdownMenuContent> <DropdownMenuItem onClick={() => setTheme("light")}> <Icons.sun className="mr-2 h-4 w-4" /> <span>Light</span> diff --git a/components/nav.tsx b/components/nav.tsx index e4743b8..36b42a3 100644 --- a/components/nav.tsx +++ b/components/nav.tsx @@ -4,25 +4,25 @@ 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 { useSession } from "next-auth/react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { GameUnityLogo } from "./logo"; import { ModeToggle } from "./mode-toggle"; interface DashboardNavProps { - items: SidebarNavItem[] + items: SidebarNavItem[]; } export default function DashboardNav({ items }: DashboardNavProps) { - const path = usePathname() + const path = usePathname(); const { data: session } = useSession(); + if (!items?.length) { - return null + return null; } - const isLoaded = true - const user = "test" + const visibleItems = session ? items : items.slice(0, 2); return ( <nav className="grid items-start gap-2"> @@ -31,47 +31,43 @@ export default function DashboardNav({ items }: DashboardNavProps) { <GameUnityLogo className="h-8 w-8" /> </Link> </div> - {session?.user && isLoaded && user ? - (items.map((item, index) => { - const Icon = Icons[item.icon as keyof IconsType || "arrowRight"]; - if (item.title === "My Profile") { - item.href = `/${user}` - } - return ( - item.href && ( - <Link key={index} href={item.disabled ? "/" : item.href} className={index == 6 ? "mt-10" : ""}> - <span - className={cn( - "group flex items-center rounded-md px-3 py-2 font-medium hover:bg-accent hover:text-accent-foreground", - path === item.href ? "bg-accent" : "transparent", - item.disabled && "cursor-not-allowed opacity-80" - )} - > - <Icon className="mr-2 h-4 w-4" /> - <span>{item.title}</span> - </span> - </Link> - ) + {visibleItems.map((item, index) => { + const Icon = Icons[item.icon as keyof IconsType || "arrowRight"]; + if (item.title === "My Profile") { + item.href = `/${session?.user?.username}` + } + return ( + item.href && ( + <Link key={index} href={item.disabled ? "/" : item.href} className={index == 6 ? "mt-10" : ""}> + <span + className={cn( + "group flex items-center rounded-md px-3 py-2 font-medium hover:bg-accent hover:text-accent-foreground", + path === item.href ? "bg-accent" : "transparent", + item.disabled && "cursor-not-allowed opacity-80" + )} + > + <Icon className="mr-3 h-5 w-5" /> + <span>{item.title}</span> + </span> + </Link> ) - })) - : - <div className="space-x-2 space-y-2 justify-center text-center"> - <Link href="/login" className={cn(buttonVariants({ size: "lg" }))}>Log In</Link> - <Link href="/signup" className={cn(buttonVariants({ size: "lg", variant: "outline" }))}>Sign Up</Link> - <p> + ) + + })} + <ModeToggle /> + {!session && ( + <div className="mt-24 space-y-3 justify-center text-center"> + <Link href="/login" className={cn(buttonVariants({ size: "lg" }), "w-full")}> + Log In + </Link> + <Link href="/signup" className={cn(buttonVariants({ size: "lg", variant: "outline" }), "w-full")}> + Sign Up + </Link> + <h4 className="text-sm"> Unlock endless possibilities - sign up or log in to unleash the full potential of our website. - </p> + </h4> </div> - } - {session?.user && - <> - <p className="text-sky-600"> {session?.user.name}</p> - <button className=" text-red-500" onClick={() => signOut()}> - Sign Out - </button> - </> - } - <ModeToggle /> + )} </nav> - ) + ); } \ No newline at end of file diff --git a/components/search-input.tsx b/components/search-input.tsx index 940f0e5..30e96e4 100644 --- a/components/search-input.tsx +++ b/components/search-input.tsx @@ -39,7 +39,7 @@ export default function SearchInput({ className, ...props }: DocsSearchProps) { value={searchQuery} onChange={(event) => setSearchQuery(event.target.value)} /> - <Button variant="outline" size="lg" className="absolute align-middle h-8 px-2.5 mr-1"> + <Button size="lg" className="absolute align-middle h-8 px-2.5 mr-1"> <Icons.arrowRight className="h-3 w-3" /> </Button> </form> diff --git a/components/site-footer.tsx b/components/site-footer.tsx index 2a0508e..d1d87ab 100644 --- a/components/site-footer.tsx +++ b/components/site-footer.tsx @@ -4,26 +4,24 @@ import { cn } from "@/lib/utils" export function SiteFooter({ className }: React.HTMLAttributes<HTMLElement>) { return ( <footer className={cn(className)}> - <div className="container flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0"> - <div className="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0"> - <span> - <Icons.logo className="h-7 w-7 dark:hidden" /> - </span> - <Icons.logoWhite className="h-7 w-7 hidden dark:block" /> - <span className="px-3"> - <Icons.logoName className="dark:hidden" /> - <Icons.logoWhiteName className="hidden dark:block" /> - </span> - <p className="text-center text-sm leading-loose md:text-left"> - Built by - Yusuf Akgül, - Omar Kasbah, - Caner Ilaslan, - David Jakszta, - Serdar Dorak, - and Valeria Luft - with â¤ï¸ - </p> + <div className="bg-black"> + <div className="container flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0"> + <div className="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0"> + <Icons.logoWhite className="h-7 w-7" /> + <span className="pr-6"> + <Icons.logoWhiteName className="" /> + </span> + <p className="text-white text-center text-sm leading-loose md:text-left"> + Built by + Yusuf Akgül, + Omar Kasbah, + Caner Ilaslan, + David Jakszta, + Serdar Dorak, + and Valeria Luft + with â¤ï¸ + </p> + </div> </div> </div> </footer> diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 399647c..7419a7f 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -13,7 +13,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - "border border-input hover:bg-accent hover:text-accent-foreground", + "text-primary border-primary border-2 input hover:bg-accent", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", @@ -53,3 +53,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( Button.displayName = "Button" export { Button, buttonVariants } + diff --git a/lib/auth.ts b/lib/auth.ts index c899556..9a459b6 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -6,6 +6,7 @@ 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" +import { normalize } from "normalize-diacritics" export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(db as any) as Adapter, @@ -64,10 +65,44 @@ export const authOptions: NextAuthOptions = { ], secret: env.NEXTAUTH_SECRET, callbacks: { + async signIn({ user, account }) { + if (account?.provider === 'github') { + const { name, email } = user; + if (!name || !email) { + return false; + } + + let username = await normalize(name.toLowerCase().replace(/\s/g, '')); + + let isUnique = false; + while (!isUnique) { + const existingUserName = await db.user.findFirst({ + where: { + username, + NOT: { email }, + }, + }) + + if (existingUserName) { + username = `${username}${Math.floor(Math.random() * 1000)}` + } else { + isUnique = true; + } + } + + await db.user.update({ + where: { email }, + data: { username }, + }); + } + + return true; + }, async session({ token, session }) { if (token) { session.user.id = token.id session.user.name = token.name + session.user.username = token.username session.user.email = token.email session.user.image = token.picture } @@ -91,6 +126,7 @@ export const authOptions: NextAuthOptions = { return { id: dbUser.id, name: dbUser.name, + username: dbUser.username, email: dbUser.email, picture: dbUser.image, } diff --git a/package-lock.json b/package-lock.json index f3aa7f9..fc05649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,11 @@ "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", - "lucide-react": "^0.234.0", + "lucide-react": "^0.236.0", "next": "^13.4.4", "next-auth": "^4.22.1", "next-themes": "^0.2.1", + "normalize-diacritics": "^4.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.44.3", @@ -4047,9 +4048,9 @@ } }, "node_modules/lucide-react": { - "version": "0.234.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.234.0.tgz", - "integrity": "sha512-7MtwK9zPXyvpHv9Tf0anraIjI9yRDdkfYIPt8KOC54NvimBAxbNpeIBb5/f+tZVIkN0hMgZdrAFugZe4YPZHcA==", + "version": "0.236.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.236.0.tgz", + "integrity": "sha512-himeKF7nVgOQ1BNcyBgk41E4/rcbmI6Zw8Q4o57nlynsFvIBA/DMacFpzKYdcyBReUj8jf08xTnCGyn/niLvwQ==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -4367,6 +4368,21 @@ "node": ">=6" } }, + "node_modules/normalize-diacritics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/normalize-diacritics/-/normalize-diacritics-4.0.0.tgz", + "integrity": "sha512-PXJtdbPvDvthxCqsi/WyQ58pPqaOKQBxuEPFmkVRMQwHTMk4aJa2gLrh5H3455JzNO0tPCM0Paj/u+CH5Jjcuw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 14.x", + "npm": ">= 6.x" + }, + "funding": { + "url": "https://github.com/motss/lit-ntml?sponsor=1" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 926c0c1..5372fd5 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,11 @@ "bcrypt": "^5.1.0", "class-variance-authority": "^0.6.0", "clsx": "^1.2.1", - "lucide-react": "^0.234.0", + "lucide-react": "^0.236.0", "next": "^13.4.4", "next-auth": "^4.22.1", "next-themes": "^0.2.1", + "normalize-diacritics": "^4.0.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.44.3", @@ -51,4 +52,4 @@ "tailwindcss": "3.3.2", "typescript": "^5.1.3" } -} \ No newline at end of file +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c9a096a..d48c1ca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,7 +47,7 @@ model Session { model User { id String @id @default(cuid()) name String? - username String? @unique @map("usernames") + username String? @unique email String? @unique emailVerified DateTime? password String? diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index 19ff225..4ca4911 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -2,17 +2,20 @@ import { User } from "next-auth" import { JWT } from "next-auth/jwt" type UserId = string +type UserUsername = string | null | undefined declare module "next-auth/jwt" { interface JWT { - id: UserId + id: UserId, + username: UserUsername } } declare module "next-auth" { interface Session { user: User & { - id: UserId + id: UserId, + username: UserUsername } } } \ No newline at end of file -- GitLab