Skip to content

Commit a53cf01

Browse files
add login with authentication and sign out
1 parent 8151ddb commit a53cf01

File tree

5 files changed

+215
-65
lines changed

5 files changed

+215
-65
lines changed

app/lib/actions.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { z } from 'zod';
44
import { sql } from '@vercel/postgres';
55
import { revalidatePath } from 'next/cache';
66
import { redirect } from 'next/navigation';
7+
import { signIn } from '@/auth';
8+
import { AuthError } from 'next-auth';
79

810
export type State = {
911
errors?: {
@@ -104,3 +106,22 @@ export async function deleteInvoice(id: string) {
104106
return { message: 'Database Error: Failed to Delete Invoice.' };
105107
}
106108
}
109+
110+
export async function authenticate(
111+
prevState: string | undefined,
112+
formData: FormData,
113+
) {
114+
try {
115+
await signIn('credentials', formData);
116+
} catch (error) {
117+
if (error instanceof AuthError) {
118+
switch (error.type) {
119+
case 'CredentialsSignin':
120+
return 'Invalid credentials.';
121+
default:
122+
return 'Something went wrong.';
123+
}
124+
}
125+
throw error;
126+
}
127+
}

app/ui/dashboard/sidenav.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from 'next/link';
22
import NavLinks from '@/app/ui/dashboard/nav-links';
33
import AcmeLogo from '@/app/ui/acme-logo';
44
import { PowerIcon } from '@heroicons/react/24/outline';
5+
import { signOut } from '@/auth';
56

67
export default function SideNav() {
78
return (
@@ -17,7 +18,11 @@ export default function SideNav() {
1718
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
1819
<NavLinks />
1920
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
20-
<form>
21+
<form
22+
action={async () => {
23+
'use server';
24+
await signOut();
25+
}}>
2126
<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">
2227
<PowerIcon className="w-6" />
2328
<div className="hidden md:block">Sign Out</div>

app/ui/login-form.tsx

Lines changed: 84 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,93 @@
1-
import { lusitana } from '@/app/ui/fonts';
1+
'use client';
2+
3+
import {lusitana} from '@/app/ui/fonts';
24
import {
3-
AtSymbolIcon,
4-
KeyIcon,
5-
ExclamationCircleIcon,
5+
AtSymbolIcon,
6+
KeyIcon,
7+
ExclamationCircleIcon,
68
} from '@heroicons/react/24/outline';
7-
import { ArrowRightIcon } from '@heroicons/react/20/solid';
8-
import { Button } from './button';
9+
import {ArrowRightIcon} from '@heroicons/react/20/solid';
10+
import {Button} from './button';
11+
import {useFormState, useFormStatus} from "react-dom";
12+
import { authenticate } from '@/app/lib/actions'
913

1014
export default function LoginForm() {
11-
return (
12-
<form className="space-y-3">
13-
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
14-
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
15-
Please log in to continue.
16-
</h1>
17-
<div className="w-full">
18-
<div>
19-
<label
20-
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
21-
htmlFor="email"
22-
>
23-
Email
24-
</label>
25-
<div className="relative">
26-
<input
27-
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
28-
id="email"
29-
type="email"
30-
name="email"
31-
placeholder="Enter your email address"
32-
required
33-
/>
34-
<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" />
35-
</div>
36-
</div>
37-
<div className="mt-4">
38-
<label
39-
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
40-
htmlFor="password"
41-
>
42-
Password
43-
</label>
44-
<div className="relative">
45-
<input
46-
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
47-
id="password"
48-
type="password"
49-
name="password"
50-
placeholder="Enter password"
51-
required
52-
minLength={6}
53-
/>
54-
<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" />
15+
const [errorMessage, dispatch] = useFormState(authenticate, undefined)
16+
17+
return (
18+
<form action={dispatch} className="space-y-3">
19+
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
20+
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
21+
Please log in to continue.
22+
</h1>
23+
<div className="w-full">
24+
<div>
25+
<label
26+
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
27+
htmlFor="email"
28+
>
29+
Email
30+
</label>
31+
<div className="relative">
32+
<input
33+
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
34+
id="email"
35+
type="email"
36+
name="email"
37+
placeholder="Enter your email address"
38+
required
39+
/>
40+
<AtSymbolIcon
41+
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"/>
42+
</div>
43+
</div>
44+
<div className="mt-4">
45+
<label
46+
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
47+
htmlFor="password"
48+
>
49+
Password
50+
</label>
51+
<div className="relative">
52+
<input
53+
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
54+
id="password"
55+
type="password"
56+
name="password"
57+
placeholder="Enter password"
58+
required
59+
minLength={6}
60+
/>
61+
<KeyIcon
62+
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"/>
63+
</div>
64+
</div>
65+
</div>
66+
<LoginButton/>
67+
<div className="flex h-8 items-end space-x-1"
68+
aria-live="polite"
69+
aria-atomic="true"
70+
>
71+
{errorMessage && (
72+
<>
73+
<ExclamationCircleIcon className="h-5 w-5 text-red-500"/>
74+
<span className="text-sm text-red-500">{errorMessage}</span>
75+
</>
76+
77+
)}
78+
</div>
5579
</div>
56-
</div>
57-
</div>
58-
<LoginButton />
59-
<div className="flex h-8 items-end space-x-1">
60-
{/* Add form errors here */}
61-
</div>
62-
</div>
63-
</form>
64-
);
80+
</form>
81+
)
82+
;
6583
}
6684

6785
function LoginButton() {
68-
return (
69-
<Button className="mt-4 w-full">
70-
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
71-
</Button>
72-
);
86+
const { pending } = useFormStatus();
87+
88+
return (
89+
<Button className="mt-4 w-full" aria-disabled={pending}>
90+
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50"/>
91+
</Button>
92+
);
7393
}

package-lock.json

Lines changed: 103 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"bcrypt": "^5.1.1",
1919
"clsx": "^2.0.0",
2020
"next": "^14.0.2",
21+
"next-auth": "^5.0.0-beta.5",
2122
"postcss": "8.4.31",
2223
"react": "18.2.0",
2324
"react-dom": "18.2.0",

0 commit comments

Comments
 (0)