From 6315119c8d5a2ad40380c6a60c5c5e049804e953 Mon Sep 17 00:00:00 2001 From: Iceship Date: Thu, 19 Sep 2024 22:49:53 +0900 Subject: [PATCH] feat: Add next-auth middleware and configure profile route - Add next-auth middleware to handle authentication - Configure profile route to require authentication - Update package.json to include next-auth dependency - Delete unused client environment file - Update server environment file with required variables - Create auth configuration file with Google provider - Add ExampleThing component and update settings - Update layout component to use Suspense for better loading experience - Create loading component with circular progress indicator - Create profile page component to display user information - Create theme switcher component for toggling between light and dark themes - Update providers component to include session provider - Delete old theme switcher component - Delete old app-navbar component - Create new app-navbar component with authentication button - Create auth-button component for handling authentication actions --- package-lock.json | 150 ++++++++++++++++++ package.json | 1 + src/app/api/auth/[...nextauth]/route.ts | 7 + src/app/layout.tsx | 3 +- src/app/loading.tsx | 13 ++ src/app/profile/page.tsx | 23 +++ src/components/app-navbar/auth-button.tsx | 63 ++++++++ .../{app-navbar.tsx => app-navbar/index.tsx} | 17 +- .../{ => app-navbar}/theme-switcher.tsx | 9 -- src/components/providers.tsx | 18 ++- src/config/auth.ts | 18 +++ src/env/client.ts | 12 -- src/env/server.ts | 4 + src/middleware.ts | 3 + 14 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/loading.tsx create mode 100644 src/app/profile/page.tsx create mode 100644 src/components/app-navbar/auth-button.tsx rename src/components/{app-navbar.tsx => app-navbar/index.tsx} (82%) rename src/components/{ => app-navbar}/theme-switcher.tsx (77%) create mode 100644 src/config/auth.ts delete mode 100644 src/env/client.ts create mode 100644 src/middleware.ts diff --git a/package-lock.json b/package-lock.json index 854c6c9..877243b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "framer-motion": "^11.5.4", "jiti": "^1.21.6", "next": "14.2.12", + "next-auth": "^4.24.7", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", @@ -2327,6 +2328,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5131,6 +5141,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7253,6 +7272,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7625,6 +7653,34 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.7", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz", + "integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.5.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "next": "^12.2.5 || ^13 || ^14", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18", + "react-dom": "^17.0.2 || ^18" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", @@ -7672,6 +7728,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7816,6 +7878,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7826,6 +7897,42 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8141,6 +8248,28 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/preact": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.0.tgz", + "integrity": "sha512-aK8Cf+jkfyuZ0ZZRG9FbYqwmEiGQ4y/PUO4SuTWoyWL244nZZh7bd5h2APd4rSNDYTBNghg1L+5iJN3Skxtbsw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8246,6 +8375,12 @@ } } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9471,6 +9606,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9680,6 +9824,12 @@ "dev": true, "license": "ISC" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", diff --git a/package.json b/package.json index d714cb6..1a5efbd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "framer-motion": "^11.5.4", "jiti": "^1.21.6", "next": "14.2.12", + "next-auth": "^4.24.7", "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0eac5e9 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,7 @@ +import NextAuth from "next-auth"; + +import options from "@/config/auth"; + +const handler = NextAuth(options); + +export { handler as GET, handler as POST }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4de1bbd..f863269 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import { Suspense } from "react"; import AppNavBar from "@/components/app-navbar"; import Providers from "@/components/providers"; @@ -27,7 +28,7 @@ export default function RootLayout({
- {children} + {children}
diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..1a491b7 --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,13 @@ +import { CircularProgress } from "@nextui-org/react"; + +export default function Loading() { + return ( + + ); +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..4787dbe --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,23 @@ +import { Card, CardBody, User } from "@nextui-org/react"; +import { getServerSession } from "next-auth"; + +import options from "@/config/auth"; + +export default async function Profile() { + const session = await getServerSession(options); + + return ( + + + + + + ); +} diff --git a/src/components/app-navbar/auth-button.tsx b/src/components/app-navbar/auth-button.tsx new file mode 100644 index 0000000..627b875 --- /dev/null +++ b/src/components/app-navbar/auth-button.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + Avatar, + Button, + CircularProgress, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownTrigger, +} from "@nextui-org/react"; +import { IconBrandGoogle } from "@tabler/icons-react"; +import { signIn, signOut, useSession } from "next-auth/react"; + +export default function AuthButton({ minimal = true }: { minimal?: boolean }) { + const { data, status } = useSession(); + if (status === "loading") { + return ; + } + if (status === "authenticated") { + const signOutClick = () => signOut({ callbackUrl: "/" }); + if (minimal) { + return ( + + ); + } + return ( + + + + + + +

Signed in as

+

{data.user?.email}

+
+ + Sign Out + +
+
+ ); + } + return ( + + ); +} diff --git a/src/components/app-navbar.tsx b/src/components/app-navbar/index.tsx similarity index 82% rename from src/components/app-navbar.tsx rename to src/components/app-navbar/index.tsx index 7544618..583ac41 100644 --- a/src/components/app-navbar.tsx +++ b/src/components/app-navbar/index.tsx @@ -13,22 +13,27 @@ import { NavbarMenuToggle, } from "@nextui-org/react"; import { IconPackage } from "@tabler/icons-react"; +import { useSession } from "next-auth/react"; +import AuthButton from "./auth-button"; import { ThemeSwitcher } from "./theme-switcher"; export default function AppNavBar() { const [isMenuOpen, setIsMenuOpen] = useState(false); + const { status } = useSession(); const menuItems = [ { label: "Home", href: "/", }, - { + ]; + if (status === "authenticated") { + menuItems.push({ label: "Profile", href: "/profile", - }, - ]; + }); + } return ( @@ -54,6 +59,9 @@ export default function AppNavBar() { + + + @@ -66,6 +74,9 @@ export default function AppNavBar() { ))} + + + ); diff --git a/src/components/theme-switcher.tsx b/src/components/app-navbar/theme-switcher.tsx similarity index 77% rename from src/components/theme-switcher.tsx rename to src/components/app-navbar/theme-switcher.tsx index 86bc40d..de6cf9b 100644 --- a/src/components/theme-switcher.tsx +++ b/src/components/app-navbar/theme-switcher.tsx @@ -1,22 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; - import { Switch } from "@nextui-org/react"; import { IconMoon, IconSun } from "@tabler/icons-react"; import useSystemTheme from "@/hooks/use-system-theme"; export function ThemeSwitcher({ showLabel }: { showLabel?: boolean }) { - const [mounted, setMounted] = useState(false); const { theme, setTheme } = useSystemTheme(); - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) return null; - return ( - {children} - + + + {children} + + ); } diff --git a/src/config/auth.ts b/src/config/auth.ts new file mode 100644 index 0000000..17ed98c --- /dev/null +++ b/src/config/auth.ts @@ -0,0 +1,18 @@ +import { NextAuthOptions } from "next-auth"; +import GoogleProvider from "next-auth/providers/google"; + +import { env } from "@/env/server"; + +const options: NextAuthOptions = { + pages: { + signIn: "/", + }, + providers: [ + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + }), + ], +}; + +export default options; diff --git a/src/env/client.ts b/src/env/client.ts deleted file mode 100644 index ed94342..0000000 --- a/src/env/client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const env = createEnv({ - client: { - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), - }, - runtimeEnv: { - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: - process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, - }, -}); diff --git a/src/env/server.ts b/src/env/server.ts index f279df8..417c115 100644 --- a/src/env/server.ts +++ b/src/env/server.ts @@ -4,6 +4,10 @@ import { ZodError, z } from "zod"; export const env = createEnv({ server: { NODE_ENV: z.enum(["development", "production"]), + NEXTAUTH_URL: z.string().url(), + NEXTAUTH_SECRET: z.string(), + GOOGLE_CLIENT_ID: z.string(), + GOOGLE_CLIENT_SECRET: z.string(), }, onValidationError: (error: ZodError) => { console.error( diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..551ca2a --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,3 @@ +export { default } from "next-auth/middleware"; + +export const config = { matcher: ["/profile"] };