From 4674ff6db236d35ef40548930672706ae972f493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 02:43:02 +0200 Subject: [PATCH 01/13] Create .env.local.example --- dashboard/15-final/.env.local.example | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 dashboard/15-final/.env.local.example diff --git a/dashboard/15-final/.env.local.example b/dashboard/15-final/.env.local.example new file mode 100644 index 00000000..b46fb133 --- /dev/null +++ b/dashboard/15-final/.env.local.example @@ -0,0 +1,2 @@ +# `openssl rand -base64 32` +AUTH_SECRET= From ddb9bd54a8f1d198f0b038687c43ef072671f046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 11:39:32 -0700 Subject: [PATCH 02/13] changes --- dashboard/15-final/app/lib/definitions.ts | 2 +- .../15-final/app/lib/placeholder-data.js | 2 +- dashboard/15-final/app/login/page.tsx | 76 +++++++++++++++++- .../app/ui/dashboard/log-out-button.tsx | 18 ----- .../15-final/app/ui/dashboard/sidenav.tsx | 15 +++- dashboard/15-final/app/ui/login-form.tsx | 77 ------------------- dashboard/15-final/auth.config.ts | 10 +-- dashboard/15-final/auth.ts | 26 ++----- dashboard/15-final/middleware.ts | 2 + dashboard/15-final/scripts/seed.js | 2 +- 10 files changed, 103 insertions(+), 127 deletions(-) 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/app/lib/definitions.ts b/dashboard/15-final/app/lib/definitions.ts index 6c6aa639..af7b11b9 100644 --- a/dashboard/15-final/app/lib/definitions.ts +++ b/dashboard/15-final/app/lib/definitions.ts @@ -3,7 +3,7 @@ // For simplicity of teaching, we're manually defining these types. // However, you're using an ORM such as Prisma, these types are generated automatically. 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 312891a5..1b7f87ee 100644 --- a/dashboard/15-final/app/lib/placeholder-data.js +++ b/dashboard/15-final/app/lib/placeholder-data.js @@ -1,7 +1,7 @@ // This file contains placeholder data that you'll be replacing with real data in Chapter 7. const users = [ { - id: 1, + id: '410544b2-4001-4271-9855-68f1c4f65645', name: 'User', email: 'user@nextmail.com', password: '123456', diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index f0743ff1..e082d720 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -1,9 +1,77 @@ -import LoginForm from '@/app/ui/login-form'; +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 { ArrowRightIcon } from '@heroicons/react/20/solid'; +import { Button } from '../ui/button'; -export default async function Page() { +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/app/ui/dashboard/log-out-button.tsx b/dashboard/15-final/app/ui/dashboard/log-out-button.tsx deleted file mode 100644 index 0351ceea..00000000 --- a/dashboard/15-final/app/ui/dashboard/log-out-button.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { PowerIcon } from '@heroicons/react/24/outline'; -import { signOut } from '@/auth'; - -export default function LogOutButton() { - return ( -
{ - 'use server'; - await signOut(); - }} - > - -
- ); -} diff --git a/dashboard/15-final/app/ui/dashboard/sidenav.tsx b/dashboard/15-final/app/ui/dashboard/sidenav.tsx index 4dd726cc..11f0408d 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 215f7923..00000000 --- a/dashboard/15-final/app/ui/login-form.tsx +++ /dev/null @@ -1,77 +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 2dc183e4..4fa7a849 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 5683e51c..56dd5273 100644 --- a/dashboard/15-final/auth.ts +++ b/dashboard/15-final/auth.ts @@ -20,31 +20,21 @@ 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; - } - - const { email, password } = validatedCredentials.data; - const user = await getUser(email); - const passwordsMatch = await bcrypt.compare(password, user.password); + if (parsedCredentials.success) { + const { email, password } = parsedCredentials.data; + const user = await getUser(email); + const passwordsMatch = await bcrypt.compare(password, user.password); - if (!passwordsMatch) { - console.log('Invalid credentials'); - return null; + if (passwordsMatch) return user; } - 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 7afd4f6d..3ffa8fc3 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/scripts/seed.js b/dashboard/15-final/scripts/seed.js index d200d3c6..af05f1c8 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 From 962fe4507385f321f6cc543768f5783faba64a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 20:42:34 +0200 Subject: [PATCH 03/13] Update page.tsx --- dashboard/15-final/app/login/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index e082d720..0c274ee0 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -5,7 +5,7 @@ import { AtSymbolIcon, KeyIcon } from '@heroicons/react/24/outline'; import { ArrowRightIcon } from '@heroicons/react/20/solid'; import { Button } from '../ui/button'; -export default async function LoginForm() { +export default async function Page() { return (
From a7b77d183d46d14fda5959c7418ad22288f4852b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 20:43:13 +0200 Subject: [PATCH 04/13] Update definitions.ts --- dashboard/15-final/app/lib/definitions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/15-final/app/lib/definitions.ts b/dashboard/15-final/app/lib/definitions.ts index af7b11b9..610eb546 100644 --- a/dashboard/15-final/app/lib/definitions.ts +++ b/dashboard/15-final/app/lib/definitions.ts @@ -1,7 +1,7 @@ -// 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: string; name: string; From f65e31d40e0ca045b9a55cab71fe8b386ef6c456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 12:38:32 -0700 Subject: [PATCH 05/13] wip signin validation --- dashboard/15-final/app/lib/actions.ts | 16 ++++++++++++++++ dashboard/15-final/app/lib/placeholder-data.js | 1 + dashboard/15-final/app/login/page.tsx | 17 ++++++++--------- dashboard/15-final/auth.ts | 6 ++++-- dashboard/15-final/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/dashboard/15-final/app/lib/actions.ts b/dashboard/15-final/app/lib/actions.ts index 1ffc9a87..e5dbf88e 100644 --- a/dashboard/15-final/app/lib/actions.ts +++ b/dashboard/15-final/app/lib/actions.ts @@ -4,6 +4,8 @@ import { z } from 'zod'; import { sql } from '@vercel/postgres'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; +import { signIn } from '@/auth'; +import { error } from 'console'; const FormSchema = z.object({ id: z.string(), @@ -119,3 +121,17 @@ export async function deleteInvoice(formData: FormData) { return { message: 'Database Error: Failed to Delete Invoice.' }; } } + +export async function authenticate( + prevState: { message?: string; errors?: string[] }, + formData: FormData, +) { + try { + await signIn('credentials', Object.fromEntries(formData)); + return {}; + } catch (error) { + // if ((error as Error).name === 'CredentialsSignin') { + return { message: 'Invalid Credentials' }; + // } + } +} diff --git a/dashboard/15-final/app/lib/placeholder-data.js b/dashboard/15-final/app/lib/placeholder-data.js index 1b7f87ee..5b529a08 100644 --- a/dashboard/15-final/app/lib/placeholder-data.js +++ b/dashboard/15-final/app/lib/placeholder-data.js @@ -5,6 +5,7 @@ const users = [ name: 'User', email: 'user@nextmail.com', password: '123456', + CredentialsSignin, }, ]; diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index e082d720..585a8494 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -1,11 +1,16 @@ -import { signIn } from '@/auth'; +'use client'; +import { useFormState } from 'react-dom'; import { lusitana } from '@/app/ui/fonts'; import AcmeLogo from '@/app/ui/acme-logo'; import { AtSymbolIcon, KeyIcon } from '@heroicons/react/24/outline'; import { ArrowRightIcon } from '@heroicons/react/20/solid'; import { Button } from '../ui/button'; +import { authenticate } from '../lib/actions'; + +export default function LoginForm() { + const initialState = {}; + const [state, dispatch] = useFormState(authenticate, initialState); -export default async function LoginForm() { return (
@@ -14,13 +19,7 @@ export default async function LoginForm() {
-
{ - 'use server'; - await signIn('credentials', Object.fromEntries(formData)); - }} - className="space-y-3" - > +

Please log in to continue. diff --git a/dashboard/15-final/auth.ts b/dashboard/15-final/auth.ts index 56dd5273..30cd8fc3 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]; @@ -27,9 +27,11 @@ export const { auth, signIn, signOut } = NextAuth({ if (parsedCredentials.success) { const { email, password } = parsedCredentials.data; + const user = await getUser(email); - const passwordsMatch = await bcrypt.compare(password, user.password); + if (!user) return null; + const passwordsMatch = await bcrypt.compare(password, user.password); if (passwordsMatch) return user; } diff --git a/dashboard/15-final/package.json b/dashboard/15-final/package.json index 35dcd0d3..11b52947 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/pnpm-lock.yaml b/pnpm-lock.yaml index 95685ac7..a561f47b 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 From 73302da646283d4582752506149865f52b549163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 25 Oct 2023 12:42:01 -0700 Subject: [PATCH 06/13] fix validation in action --- dashboard/15-final/app/lib/actions.ts | 6 +++--- dashboard/15-final/app/login/page.tsx | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dashboard/15-final/app/lib/actions.ts b/dashboard/15-final/app/lib/actions.ts index e5dbf88e..9d78c954 100644 --- a/dashboard/15-final/app/lib/actions.ts +++ b/dashboard/15-final/app/lib/actions.ts @@ -130,8 +130,8 @@ export async function authenticate( await signIn('credentials', Object.fromEntries(formData)); return {}; } catch (error) { - // if ((error as Error).name === 'CredentialsSignin') { - return { message: 'Invalid Credentials' }; - // } + if ((error as Error).message.includes('CredentialsSignin')) { + return { message: 'Invalid Credentials' }; + } } } diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index 585a8494..2bb7b37b 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -65,6 +65,9 @@ export default function LoginForm() {

+

+ {state.message} +

+ ); +} diff --git a/dashboard/15-final/app/login/page.tsx b/dashboard/15-final/app/login/page.tsx index ee4b0edd..b8b49a3d 100644 --- a/dashboard/15-final/app/login/page.tsx +++ b/dashboard/15-final/app/login/page.tsx @@ -1,15 +1,7 @@ -'use client'; -import { useFormState } from 'react-dom'; -import { lusitana } from '@/app/ui/fonts'; import AcmeLogo from '@/app/ui/acme-logo'; -import { AtSymbolIcon, KeyIcon } from '@heroicons/react/24/outline'; -import { ArrowRightIcon } from '@heroicons/react/20/solid'; -import { Button } from '../ui/button'; -import { authenticate } from '../lib/actions'; - -export default function LoginForm() { - const [code, action] = useFormState(authenticate, undefined); +import LoginForm from './form'; +export default function LoginPage() { return (
@@ -18,60 +10,7 @@ export default function LoginForm() {
-
-
-

- Please log in to continue. -

-
-
- -
- - -
-
-
- -
- - -
-
-
-

- {code === 'CredentialsSignin' ? 'Invalid credentials' : ''} -

-
- -
+
); diff --git a/dashboard/15-final/app/ui/button.tsx b/dashboard/15-final/app/ui/button.tsx index 47d15798..af8f627b 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) { );