Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match NextAuth config with the course #221

Merged
merged 16 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dashboard/15-final/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# `openssl rand -base64 32`
AUTH_SECRET=
15 changes: 15 additions & 0 deletions dashboard/15-final/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}
}
6 changes: 3 additions & 3 deletions dashboard/15-final/app/lib/definitions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
3 changes: 2 additions & 1 deletion dashboard/15-final/app/lib/placeholder-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
];

Expand Down
76 changes: 76 additions & 0 deletions dashboard/15-final/app/login/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { authenticate } from '../lib/actions';
import { lusitana } from '@/app/ui/fonts';
import { AtSymbolIcon, KeyIcon } 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 (
<form action={action} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-6 pt-5">
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
Please log in to continue.
</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="email"
>
Email
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="email"
type="email"
name="email"
placeholder="Enter your email address"
required
/>
<AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password"
required
minLength={6}
/>
<KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
</div>
<p aria-live="polite" className="mt-2 text-sm text-red-500">
{code === 'CredentialsSignin' ? 'Invalid credentials' : ''}
</p>
<LoginButton />
</div>
</form>
);
}

function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
}
16 changes: 12 additions & 4 deletions dashboard/15-final/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main>
<LoginForm />
<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>
);
}
2 changes: 1 addition & 1 deletion dashboard/15-final/app/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function Button({ children, className, ...rest }: ButtonProps) {
<button
{...rest}
className={clsx(
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500',
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
className,
)}
>
Expand Down
18 changes: 0 additions & 18 deletions dashboard/15-final/app/ui/dashboard/log-out-button.tsx

This file was deleted.

15 changes: 13 additions & 2 deletions dashboard/15-final/app/ui/dashboard/sidenav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -17,7 +18,17 @@ export default function SideNav() {
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<LogOutButton />
<form
action={async () => {
'use server';
await signOut();
}}
>
<button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
</button>
</form>
</div>
</div>
);
Expand Down
75 changes: 0 additions & 75 deletions dashboard/15-final/app/ui/login-form.tsx

This file was deleted.

10 changes: 5 additions & 5 deletions dashboard/15-final/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
28 changes: 10 additions & 18 deletions dashboard/15-final/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<User | undefined> {
try {
const user = await sql<User>`SELECT * from USERS where email=${email}`;
return user.rows[0];
Expand All @@ -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 { email, password } = validatedCredentials.data;
const user = await getUser(email);
const passwordsMatch = await bcrypt.compare(password, user.password);
const user = await getUser(email);
if (!user) return null;

if (!passwordsMatch) {
console.log('Invalid credentials');
return null;
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) return user;
}

return { ...user, id: user.id.toString() };
console.log('Invalid credentials');
return null;
},
}),
],
Expand Down
2 changes: 2 additions & 0 deletions dashboard/15-final/middleware.ts
Original file line number Diff line number Diff line change
@@ -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).*)'],
};
2 changes: 1 addition & 1 deletion dashboard/15-final/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion dashboard/15-final/scripts/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading