From f8adecb7a41cb37ab6506bd7c3a0aa1aeb085178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 15:53:07 -0700 Subject: [PATCH] Match NextAuth config with the course (#221) * Create .env.local.example * changes * Update page.tsx * Update definitions.ts * wip signin validation * fix validation in action * fix * remove import * change * update * change * added button state * improved UI --------- Co-authored-by: Steven Tey --- dashboard/15-final/.env.local.example | 2 + dashboard/15-final/app/lib/actions.ts | 15 ++++ dashboard/15-final/app/lib/definitions.ts | 6 +- .../15-final/app/lib/placeholder-data.js | 3 +- dashboard/15-final/app/login/form.tsx | 87 +++++++++++++++++++ dashboard/15-final/app/login/page.tsx | 16 +++- dashboard/15-final/app/ui/button.tsx | 2 +- .../app/ui/dashboard/log-out-button.tsx | 18 ---- .../15-final/app/ui/dashboard/sidenav.tsx | 15 +++- dashboard/15-final/app/ui/login-form.tsx | 75 ---------------- dashboard/15-final/auth.config.ts | 10 +-- dashboard/15-final/auth.ts | 32 +++---- dashboard/15-final/middleware.ts | 2 + dashboard/15-final/package.json | 2 +- dashboard/15-final/scripts/seed.js | 2 +- pnpm-lock.yaml | 8 +- 16 files changed, 160 insertions(+), 135 deletions(-) create mode 100644 dashboard/15-final/.env.local.example create mode 100644 dashboard/15-final/app/login/form.tsx delete mode 100644 dashboard/15-final/app/ui/dashboard/log-out-button.tsx delete mode 100644 dashboard/15-final/app/ui/login-form.tsx diff --git a/dashboard/15-final/.env.local.example b/dashboard/15-final/.env.local.example new file mode 100644 index 0000000..b46fb13 --- /dev/null +++ b/dashboard/15-final/.env.local.example @@ -0,0 +1,2 @@ +# `openssl rand -base64 32` +AUTH_SECRET= diff --git a/dashboard/15-final/app/lib/actions.ts b/dashboard/15-final/app/lib/actions.ts index 08d14f4..1a318db 100644 --- a/dashboard/15-final/app/lib/actions.ts +++ b/dashboard/15-final/app/lib/actions.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { sql } from '@vercel/postgres'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; +import { signIn } from '@/auth'; const FormSchema = z.object({ id: z.string(), @@ -119,3 +120,17 @@ export async function deleteInvoice(formData: FormData) { return { message: 'Database Error: Failed to Delete Invoice.' }; } } + +export async function authenticate( + prevState: string | undefined, + formData: FormData, +) { + try { + await signIn('credentials', Object.fromEntries(formData)); + } catch (error) { + if ((error as Error).message.includes('CredentialsSignin')) { + return 'CredentialsSignin'; + } + throw error; + } +} diff --git a/dashboard/15-final/app/lib/definitions.ts b/dashboard/15-final/app/lib/definitions.ts index 6c6aa63..610eb54 100644 --- a/dashboard/15-final/app/lib/definitions.ts +++ b/dashboard/15-final/app/lib/definitions.ts @@ -1,9 +1,9 @@ -// This file contains type definitions for you data. +// This file contains type definitions for your data. // It describes the shape of the data, and what data type each property should accept. // For simplicity of teaching, we're manually defining these types. -// However, you're using an ORM such as Prisma, these types are generated automatically. +// However, these types are generated automatically if you're using an ORM such as Prisma. export type User = { - id: number; + id: string; name: string; email: string; password: string; diff --git a/dashboard/15-final/app/lib/placeholder-data.js b/dashboard/15-final/app/lib/placeholder-data.js index 897ca47..5f0f9da 100644 --- a/dashboard/15-final/app/lib/placeholder-data.js +++ b/dashboard/15-final/app/lib/placeholder-data.js @@ -2,10 +2,11 @@ // https://nextjs.org/learn/dashboard-app/fetching-data const users = [ { - id: 1, + id: '410544b2-4001-4271-9855-68f1c4f65645', name: 'User', email: 'user@nextmail.com', password: '123456', + CredentialsSignin, }, ]; diff --git a/dashboard/15-final/app/login/form.tsx b/dashboard/15-final/app/login/form.tsx new file mode 100644 index 0000000..7961ac4 --- /dev/null +++ b/dashboard/15-final/app/login/form.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { useFormState, useFormStatus } from 'react-dom'; +import { authenticate } from '../lib/actions'; +import { lusitana } from '@/app/ui/fonts'; +import { + AtSymbolIcon, + KeyIcon, + ExclamationCircleIcon, +} from '@heroicons/react/24/outline'; +import { ArrowRightIcon } from '@heroicons/react/20/solid'; +import { Button } from '../ui/button'; + +export default function LoginForm() { + const [code, action] = useFormState(authenticate, undefined); + + return ( +
+
+

+ Please log in to continue. +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ {code === 'CredentialsSignin' && ( + <> + +

+ Invalid credentials +

+ + )} +
+
+
+ ); +} + +function LoginButton() { + const { pending } = useFormStatus(); + return ( + + ); +} diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index f0743ff..b8b49a3 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -1,9 +1,17 @@ -import LoginForm from '@/app/ui/login-form'; +import AcmeLogo from '@/app/ui/acme-logo'; +import LoginForm from './form'; -export default async function Page() { +export default function LoginPage() { return ( -
- +
+
+
+
+ +
+
+ +
); } diff --git a/dashboard/15-final/app/ui/button.tsx b/dashboard/15-final/app/ui/button.tsx index 47d1579..af8f627 100644 --- a/dashboard/15-final/app/ui/button.tsx +++ b/dashboard/15-final/app/ui/button.tsx @@ -9,7 +9,7 @@ export function Button({ children, className, ...rest }: ButtonProps) { - - ); -} diff --git a/dashboard/15-final/app/ui/dashboard/sidenav.tsx b/dashboard/15-final/app/ui/dashboard/sidenav.tsx index 4dd726c..11f0408 100644 --- a/dashboard/15-final/app/ui/dashboard/sidenav.tsx +++ b/dashboard/15-final/app/ui/dashboard/sidenav.tsx @@ -1,7 +1,8 @@ import Link from 'next/link'; import NavLinks from '@/app/ui/dashboard/nav-links'; -import LogOutButton from './log-out-button'; import AcmeLogo from '../acme-logo'; +import { PowerIcon } from '@heroicons/react/24/outline'; +import { signOut } from '@/auth'; export default function SideNav() { return ( @@ -17,7 +18,17 @@ export default function SideNav() {
- +
{ + 'use server'; + await signOut(); + }} + > + +
); diff --git a/dashboard/15-final/app/ui/login-form.tsx b/dashboard/15-final/app/ui/login-form.tsx deleted file mode 100644 index bd7422f..0000000 --- a/dashboard/15-final/app/ui/login-form.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { signIn } from '@/auth'; -import { lusitana } from '@/app/ui/fonts'; -import AcmeLogo from '@/app/ui/acme-logo'; -import { AtSymbolIcon, KeyIcon } from '@heroicons/react/24/outline'; -import { Button } from './button'; -import { ArrowRightIcon } from '@heroicons/react/20/solid'; - -export default async function LoginForm() { - return ( -
-
-
- -
-
{ - 'use server'; - await signIn('credentials', Object.fromEntries(formData)); - }} - className="space-y-3" - > -
-

- Please log in to continue. -

-
-
- -
- - -
-
-
- -
- - -
-
-
-
- -
-
-
- ); -} diff --git a/dashboard/15-final/auth.config.ts b/dashboard/15-final/auth.config.ts index 2dc183e..4fa7a84 100644 --- a/dashboard/15-final/auth.config.ts +++ b/dashboard/15-final/auth.config.ts @@ -1,6 +1,9 @@ import type { NextAuthConfig } from 'next-auth'; export const authConfig = { + pages: { + signIn: '/login', + }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js // while this file is also used in non-Node.js environments @@ -10,15 +13,12 @@ export const authConfig = { const isLoggedIn = !!auth?.user; const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); if (isOnDashboard) { - if (!isLoggedIn) return Response.redirect(new URL('/login', nextUrl)); - return true; + if (isLoggedIn) return true; + return false; // Redirect unathenticated users to login page } else if (isLoggedIn) { return Response.redirect(new URL('/dashboard', nextUrl)); } return true; }, }, - pages: { - signIn: '/login', - }, } satisfies NextAuthConfig; diff --git a/dashboard/15-final/auth.ts b/dashboard/15-final/auth.ts index 5683e51..30cd8fc 100644 --- a/dashboard/15-final/auth.ts +++ b/dashboard/15-final/auth.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { User } from '@/app/lib/definitions'; import { authConfig } from './auth.config'; -async function getUser(email: string) { +async function getUser(email: string): Promise { try { const user = await sql`SELECT * from USERS where email=${email}`; return user.rows[0]; @@ -20,31 +20,23 @@ export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [ Credentials({ - name: 'Sign-In with Credentials', - credentials: { - password: { label: 'Password', type: 'password' }, - email: { label: 'Email', type: 'email' }, - }, async authorize(credentials) { - const validatedCredentials = z + const parsedCredentials = z .object({ email: z.string().email(), password: z.string().min(6) }) .safeParse(credentials); - if (!validatedCredentials.success) { - console.log('Invalid credentials'); - return null; + if (parsedCredentials.success) { + const { email, password } = parsedCredentials.data; + + const user = await getUser(email); + if (!user) return null; + + const passwordsMatch = await bcrypt.compare(password, user.password); + if (passwordsMatch) return user; } - const { email, password } = validatedCredentials.data; - const user = await getUser(email); - const passwordsMatch = await bcrypt.compare(password, user.password); - - if (!passwordsMatch) { - console.log('Invalid credentials'); - return null; - } - - return { ...user, id: user.id.toString() }; + console.log('Invalid credentials'); + return null; }, }), ], diff --git a/dashboard/15-final/middleware.ts b/dashboard/15-final/middleware.ts index 7afd4f6..3ffa8fc 100644 --- a/dashboard/15-final/middleware.ts +++ b/dashboard/15-final/middleware.ts @@ -1,7 +1,9 @@ import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; + export default NextAuth(authConfig).auth; export const config = { + // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }; diff --git a/dashboard/15-final/package.json b/dashboard/15-final/package.json index 35dcd0d..11b5294 100644 --- a/dashboard/15-final/package.json +++ b/dashboard/15-final/package.json @@ -12,7 +12,7 @@ "@tailwindcss/forms": "^0.5.6", "@types/node": "20.5.7", "@types/react": "18.2.21", - "@types/react-dom": "18.2.7", + "@types/react-dom": "18.2.14", "@vercel/postgres": "^0.5.0", "autoprefixer": "10.4.15", "bcrypt": "^5.1.1", diff --git a/dashboard/15-final/scripts/seed.js b/dashboard/15-final/scripts/seed.js index d200d3c..af05f1c 100644 --- a/dashboard/15-final/scripts/seed.js +++ b/dashboard/15-final/scripts/seed.js @@ -12,7 +12,7 @@ async function seedUsers() { // Create the "invoices" table if it doesn't exist const createTable = await sql` CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, name VARCHAR(255) NOT NULL, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95685ac..a561f47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,8 +226,8 @@ importers: specifier: 18.2.21 version: 18.2.21 '@types/react-dom': - specifier: 18.2.7 - version: 18.2.7 + specifier: 18.2.14 + version: 18.2.14 '@vercel/postgres': specifier: ^0.5.0 version: 0.5.0 @@ -1036,8 +1036,8 @@ packages: /@types/prop-types@15.7.9: resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==} - /@types/react-dom@18.2.7: - resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} + /@types/react-dom@18.2.14: + resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} dependencies: '@types/react': 18.2.21 dev: false