implement first half of tutorial
start from "Updating the login form"
This commit is contained in:
		
							parent
							
								
									89e338a0ac
								
							
						
					
					
						commit
						67af7fac8c
					
				
							
								
								
									
										2
									
								
								.env
								
								
								
								
							
							
						
						
									
										2
									
								
								.env
								
								
								
								
							|  | @ -5,3 +5,5 @@ | ||||||
| # See the documentation for all the connection string options: https://pris.ly/d/connection-strings | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings | ||||||
| 
 | 
 | ||||||
| DATABASE_URL="file:./dev.db" | DATABASE_URL="file:./dev.db" | ||||||
|  | AUTH_SECRET="fjvZUGDnuNa4/9kUxrIytM45gHiQhLTvSsdvsMcB/Rg=" | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | import type { NextAuthConfig } from 'next-auth'; | ||||||
|  | 
 | ||||||
|  | export const authConfig = { | ||||||
|  | 	pages: { | ||||||
|  | 		signIn: '/login', | ||||||
|  | 	}, | ||||||
|  | 	callbacks: { | ||||||
|  | 		authorized({ auth, request: { nextUrl } }) { | ||||||
|  | 			const isLoggedIn = !!auth?.user; | ||||||
|  | 			const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); | ||||||
|  | 			if (isOnDashboard) { | ||||||
|  | 				if (isLoggedIn) return true; | ||||||
|  | 				return false; // Redirect unauthenticated users to login page
 | ||||||
|  | 			} else if (isLoggedIn) { | ||||||
|  | 				return Response.redirect(new URL('/dashboard', nextUrl)); | ||||||
|  | 			} | ||||||
|  | 			return true; | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	providers: [], // Add providers with an empty array for now
 | ||||||
|  | } satisfies NextAuthConfig; | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | import NextAuth, { User } from 'next-auth'; | ||||||
|  | import { authConfig } from './auth.config'; | ||||||
|  | import Credentials from 'next-auth/providers/credentials'; | ||||||
|  | import { z } from 'zod'; | ||||||
|  | import prisma from 'app/lib/db'; | ||||||
|  | import bcrypt from 'bcrypt'; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async function getUser(email: string): Promise<User | undefined> { | ||||||
|  | 	try { | ||||||
|  | 		const user = await prisma.user.findFirst({ | ||||||
|  | 			where: { | ||||||
|  | 				email | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 		return user | ||||||
|  | 	} catch (error) { | ||||||
|  | 		throw new Error("failed to fetch user") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export const { auth, signIn, signOut } = NextAuth({ | ||||||
|  | 	...authConfig, | ||||||
|  | 	providers: [Credentials({ | ||||||
|  | 		async authorize(credentials) { | ||||||
|  | 			const parsedCredentials = z | ||||||
|  | 				.object({ email: z.string().email(), password: z.string().min(6) }) | ||||||
|  | 				.safeParse(credentials); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 			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; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			console.log('Invalid credentials'); | ||||||
|  | 			return null; | ||||||
|  | 		}, | ||||||
|  | 	})], | ||||||
|  | }); | ||||||
|  | @ -0,0 +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|.*\\.png$).*)'], | ||||||
|  | }; | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -24,11 +24,13 @@ | ||||||
|     "@radix-ui/react-slot": "^1.0.2", |     "@radix-ui/react-slot": "^1.0.2", | ||||||
|     "@radix-ui/react-toast": "^1.1.5", |     "@radix-ui/react-toast": "^1.1.5", | ||||||
|     "@tanstack/react-table": "^8.17.3", |     "@tanstack/react-table": "^8.17.3", | ||||||
|  |     "bcrypt": "^5.1.1", | ||||||
|     "class-variance-authority": "^0.7.0", |     "class-variance-authority": "^0.7.0", | ||||||
|     "clsx": "^2.1.1", |     "clsx": "^2.1.1", | ||||||
|     "date-fns": "^3.6.0", |     "date-fns": "^3.6.0", | ||||||
|     "lucide-react": "^0.394.0", |     "lucide-react": "^0.394.0", | ||||||
|     "next": "14.2.3", |     "next": "14.2.3", | ||||||
|  |     "next-auth": "^5.0.0-beta.20", | ||||||
|     "next-themes": "^0.3.0", |     "next-themes": "^0.3.0", | ||||||
|     "react": "^18", |     "react": "^18", | ||||||
|     "react-day-picker": "^8.10.1", |     "react-day-picker": "^8.10.1", | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | -- CreateTable | ||||||
|  | CREATE TABLE "User" ( | ||||||
|  |     "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|  |     "email" TEXT NOT NULL, | ||||||
|  |     "password" TEXT NOT NULL | ||||||
|  | ); | ||||||
|  | @ -10,6 +10,12 @@ datasource db { | ||||||
|   url      = "file:./dev.db" |   url      = "file:./dev.db" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | model User { | ||||||
|  | id         Int     @id @default(autoincrement()) | ||||||
|  | email String | ||||||
|  | password String | ||||||
|  | } | ||||||
|  | 
 | ||||||
| model Story { | model Story { | ||||||
|   id         Int     @id @default(autoincrement()) |   id         Int     @id @default(autoincrement()) | ||||||
|   word_count Int |   word_count Int | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | import AcmeLogo from '@/app/ui/acme-logo'; | ||||||
|  | import LoginForm from '@/app/ui/login-form'; | ||||||
|  | 
 | ||||||
|  | export default function LoginPage() { | ||||||
|  |   return ( | ||||||
|  |     <main className="flex items-center justify-center md:h-screen"> | ||||||
|  |       <div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32"> | ||||||
|  |         <div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36"> | ||||||
|  |           <div className="w-32 text-white md:w-36"> | ||||||
|  |             <AcmeLogo /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <LoginForm /> | ||||||
|  |       </div> | ||||||
|  |     </main> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -813,6 +813,10 @@ body { | ||||||
|   height: 0.5rem; |   height: 0.5rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-20 { | ||||||
|  |   height: 5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .h-24 { | .h-24 { | ||||||
|   height: 6rem; |   height: 6rem; | ||||||
| } | } | ||||||
|  | @ -906,6 +910,10 @@ body { | ||||||
|   width: 0.875rem; |   width: 0.875rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .w-32 { | ||||||
|  |   width: 8rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .w-4 { | .w-4 { | ||||||
|   width: 1rem; |   width: 1rem; | ||||||
| } | } | ||||||
|  | @ -972,6 +980,10 @@ body { | ||||||
|   min-width: fit-content; |   min-width: fit-content; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .max-w-\[400px\] { | ||||||
|  |   max-width: 400px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .max-w-full { | .max-w-full { | ||||||
|   max-width: 100%; |   max-width: 100%; | ||||||
| } | } | ||||||
|  | @ -1076,6 +1088,10 @@ body { | ||||||
|   align-items: flex-start; |   align-items: flex-start; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .items-end { | ||||||
|  |   align-items: flex-end; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .items-center { | .items-center { | ||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  | @ -1169,6 +1185,12 @@ body { | ||||||
|   margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); |   margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .space-y-2\.5 > :not([hidden]) ~ :not([hidden]) { | ||||||
|  |   --tw-space-y-reverse: 0; | ||||||
|  |   margin-top: calc(0.625rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|  |   margin-bottom: calc(0.625rem * var(--tw-space-y-reverse)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .space-y-4 > :not([hidden]) ~ :not([hidden]) { | .space-y-4 > :not([hidden]) ~ :not([hidden]) { | ||||||
|   --tw-space-y-reverse: 0; |   --tw-space-y-reverse: 0; | ||||||
|   margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); |   margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); | ||||||
|  | @ -1201,6 +1223,10 @@ body { | ||||||
|   border-radius: 9999px; |   border-radius: 9999px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .rounded-lg { | ||||||
|  |   border-radius: var(--radius); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .rounded-md { | .rounded-md { | ||||||
|   border-radius: calc(var(--radius) - 2px); |   border-radius: calc(var(--radius) - 2px); | ||||||
| } | } | ||||||
|  | @ -1267,6 +1293,11 @@ body { | ||||||
|   background-color: rgb(0 0 0 / 0.8); |   background-color: rgb(0 0 0 / 0.8); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .bg-blue-500 { | ||||||
|  |   --tw-bg-opacity: 1; | ||||||
|  |   background-color: rgb(59 130 246 / var(--tw-bg-opacity)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .bg-border { | .bg-border { | ||||||
|   background-color: hsl(var(--border)); |   background-color: hsl(var(--border)); | ||||||
| } | } | ||||||
|  | @ -2347,6 +2378,22 @@ body { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (min-width: 768px) { | @media (min-width: 768px) { | ||||||
|  |   .md\:-mt-32 { | ||||||
|  |     margin-top: -8rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .md\:h-36 { | ||||||
|  |     height: 9rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .md\:h-screen { | ||||||
|  |     height: 100vh; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .md\:w-36 { | ||||||
|  |     width: 9rem; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   .md\:max-w-\[420px\] { |   .md\:max-w-\[420px\] { | ||||||
|     max-width: 420px; |     max-width: 420px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue