Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions app-router/nextjs-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Part of this tutorial was to deploy it out to vercel. It was very slick.I took t
12. [Mutating Data️ ♻️][2-12]
13. [Handling Errors ♻️][2-13]
14. [Improving Accessibility ♻️][2-14]
15. Adding Authentication 🚧
15. [Adding Authentication ♻️][2-15]
16. Adding Metadata 🚧️


Expand All @@ -37,4 +37,5 @@ Part of this tutorial was to deploy it out to vercel. It was very slick.I took t
[2-11]: https://github.com/treejamie/next-js-learn/pull/19
[2-12]: https://github.com/treejamie/next-js-learn/pull/20
[2-13]: https://github.com/treejamie/next-js-learn/pull/23
[2-14]: https://github.com/treejamie/next-js-learn/pull/25
[2-14]: https://github.com/treejamie/next-js-learn/pull/25
[2-15]: https://github.com/treejamie/next-js-learn/pull/28
27 changes: 26 additions & 1 deletion app-router/nextjs-dashboard/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import postgres from 'postgres';

import { signIn } from '@/auth';
import { AuthError } from 'next-auth';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

// maybe this will get solved later, but this is already
// defined in the definitions to an extent. Is this some
// stink?
// stink? Or is Zod mighty?
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
Expand All @@ -24,6 +27,28 @@ const FormSchema = z.object({
date: z.string(),
});

export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}




const CreateInvoice = FormSchema.omit({ id: true, date: true });

const UpdateInvoice = FormSchema.omit({ id: true, date: true });
Expand Down
20 changes: 20 additions & 0 deletions app-router/nextjs-dashboard/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import AcmeLogo from '@/app/ui/acme-logo';
import LoginForm from '@/app/ui/login-form';
import { Suspense } from 'react';

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>
<Suspense>
<LoginForm />
</Suspense>
</div>
</main>
);
}
8 changes: 7 additions & 1 deletion app-router/nextjs-dashboard/app/ui/dashboard/sidenav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
import AcmeLogo from '@/app/ui/acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
import { signOut } from '@/auth';

export default function SideNav() {
return (
Expand All @@ -17,7 +18,12 @@ 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>
<form>
<form
action={async () => {
'use server';
await signOut({ redirectTo: '/' });
}}
>
<button className="flex h-[48px] w-full 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>
Expand Down
36 changes: 29 additions & 7 deletions app-router/nextjs-dashboard/app/ui/login-form.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
'use client';

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 './button';

import { Button } from '@/app/ui/button';
import { useActionState } from 'react';
import { authenticate } from '@/app/lib/actions';
import { useSearchParams } from 'next/navigation';

export default function LoginForm() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
const [errorMessage, formAction, isPending] = useActionState(
authenticate,
undefined,
);

return (
<form className="space-y-3">
<form action={formAction} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
Please log in to continue.
Expand Down Expand Up @@ -55,13 +67,23 @@ export default function LoginForm() {
</div>
</div>
</div>
<Button className="mt-4 w-full">
<input type="hidden" name="redirectTo" value={callbackUrl} />
<Button className="mt-4 w-full" aria-disabled={isPending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
<div className="flex h-8 items-end space-x-1">
{/* Add form errors here */}
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
</div>
</div>
</form>
);
}
}
21 changes: 21 additions & 0 deletions app-router/nextjs-dashboard/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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;
44 changes: 44 additions & 0 deletions app-router/nextjs-dashboard/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
import postgres from 'postgres';

const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User[]>`SELECT * FROM users WHERE email=${email}`;
return user[0];
} catch (error) {
console.error('Failed to fetch user:', 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;
},
}),
],
});
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"next-auth": "5.0.0-beta.25"
}
}
Loading