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