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

Add basic auth #1

Merged
merged 29 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0b2a5f0
Rename "be" to "drizzle"
delbaoliveira Mar 13, 2024
353c474
Rename (marketing) to (public)
delbaoliveira Mar 13, 2024
079ee6e
Create form schema
delbaoliveira Mar 13, 2024
39db81d
Create auth.ts
delbaoliveira Mar 13, 2024
6aa329f
Update signup form
delbaoliveira Mar 13, 2024
a41ec5b
Create session.ts
delbaoliveira Mar 13, 2024
a5171c6
Install bcrypt, jsonwebtoken, and zod
delbaoliveira Mar 13, 2024
12fb2e8
Fix bad imports
delbaoliveira Mar 13, 2024
6bd0c86
Install server-only package
delbaoliveira Mar 13, 2024
785d16d
wip
delbaoliveira Mar 13, 2024
8e53e09
Merge branch 'main' into add-basic-auth
delbaoliveira Mar 13, 2024
c8acf0f
Run prettier
delbaoliveira Mar 13, 2024
9a05f2e
await createSession()
delbaoliveira Mar 13, 2024
f6f8771
Add verifySession() util - wip
delbaoliveira Mar 13, 2024
6f174a9
Testing
delbaoliveira Mar 13, 2024
d01cdfb
add login and logout functionality
Mar 13, 2024
1db0c64
Polish login/logout
delbaoliveira Mar 14, 2024
6c61376
Organize
delbaoliveira Mar 14, 2024
8601e86
Split client and server sessions
delbaoliveira Mar 14, 2024
1cca22a
Switch to jose (supported in the Edge runtime)
delbaoliveira Mar 14, 2024
f58caff
Set up redirects in middleware
delbaoliveira Mar 14, 2024
2eef4d1
add login query inside try catch block
Mar 14, 2024
8c402b3
Fix broken deployment
delbaoliveira Mar 19, 2024
a25de2e
Rename client/server sessions to stateless/database
delbaoliveira Mar 19, 2024
a943521
Add dal
delbaoliveira Mar 19, 2024
310dbed
handle decrypt function error
delbaoliveira Mar 19, 2024
af0f238
Update form.tsx
delbaoliveira Mar 19, 2024
77e5d2d
Redirects and tidy up a bit
delbaoliveira Mar 21, 2024
568627e
Clean up
delbaoliveira Mar 25, 2024
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
57 changes: 0 additions & 57 deletions app/(marketing)/login/page.tsx

This file was deleted.

51 changes: 0 additions & 51 deletions app/(marketing)/signup/page.tsx

This file was deleted.

File renamed without changes.
62 changes: 62 additions & 0 deletions app/(public)/login/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use client';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { login } from '@/app/auth/01-auth';
import Link from 'next/link';
import { useFormState, useFormStatus } from 'react-dom';

export function LoginForm() {
const [state, action] = useFormState(login, undefined);

return (
<form action={action}>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
placeholder="[email protected]"
type="email"
/>
{state?.errors?.email && (
<p className="text-red-500">{state.errors.email}</p>
)}
</div>
<div className="space-y-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link className="ml-auto inline-block text-sm underline" href="#">
Forgot your password?
</Link>
</div>
<Input id="password" type="password" name="password" />
{state?.errors?.password && (
<p className="text-red-500">{state.errors.password}</p>
)}
</div>
{state?.message && <p className="text-red-500">{state.message}</p>}
<LoginButton />
</div>

<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link className="underline" href="/signup">
Sign up
</Link>
</div>
</form>
);
}

export function LoginButton() {
const { pending } = useFormStatus();

return (
<Button aria-disabled={pending} type="submit">
{pending ? 'Submitting...' : 'Sign up'}
</Button>
);
}
17 changes: 17 additions & 0 deletions app/(public)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LoginForm } from './form';

export default function Page() {
return (
<div className="flex w-1/4 items-center px-4 sm:px-6 lg:px-8">
<div className="mx-auto w-full space-y-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Login</h1>
<p className="text-sm text-gray-500">
Enter your email below to login to your account
</p>
</div>
<LoginForm />
</div>
</div>
);
}
File renamed without changes.
57 changes: 57 additions & 0 deletions app/(public)/signup/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { signup } from '@/app/auth/01-auth';
import { useFormState, useFormStatus } from 'react-dom';

export function SignupForm() {
const [state, action] = useFormState(signup, undefined);

return (
<form action={action}>
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" placeholder="John Doe" />
</div>
{state?.errors?.name && (
<p className="text-red-500">{state.errors.name}</p>
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" placeholder="[email protected]" />
</div>
{state?.errors?.email && (
<p className="text-red-500">{state.errors.email}</p>
)}
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error} className="text-red-500">
- {error}
</li>
))}
</ul>
</div>
)}
<SignupButton />
</form>
);
}

export function SignupButton() {
const { pending } = useFormStatus();

return (
<Button aria-disabled={pending} type="submit">
{pending ? 'Submitting...' : 'Sign up'}
</Button>
);
}
21 changes: 21 additions & 0 deletions app/(public)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Link from 'next/link';
import { SignupForm } from '@/app/(public)/signup/form';
export default function Page() {
return (
<div className="flex w-1/4 items-center px-4 sm:px-6 lg:px-8">
<div className="mx-auto w-full space-y-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-gray-500">Enter your information to get started</p>
</div>
<SignupForm />
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link className="underline" href="/login">
Login
</Link>
</div>
</div>
</div>
);
}
121 changes: 121 additions & 0 deletions app/auth/01-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use server';

import { db } from '@/drizzle/db';
import { users } from '@/drizzle/schema';
import {
FormState,
LoginFormSchema,
SignupFormSchema,
} from '@/app/auth/definitions';
import { createSession, deleteSession } from '@/app/auth/02-stateless-session';
import bcrypt from 'bcrypt';
import { eq } from 'drizzle-orm';
import { redirect } from 'next/navigation';

export async function signup(
state: FormState,
formData: FormData,
): Promise<FormState> {
// 1. Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
});

// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data;

// 3. Check if the user's email already exists
const existingUser = await db.query.users.findFirst({
where: eq(users.email, email),
});

if (existingUser) {
return {
message: 'Email already exists, please use a different email or login.',
};
}

// Hash the user's password
const hashedPassword = await bcrypt.hash(password, 10);

// 3. Insert the user into the database or call an Auth Provider's API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id });

const user = data[0];

if (!user) {
return {
message: 'An error occurred while creating your account.',
};
}

// 4. Create a session for the user
const userId = user.id.toString();
await createSession(userId);
redirect('/dashboard');
}

export async function login(
state: FormState,
formData: FormData,
): Promise<FormState> {
// 1. Validate form fields
const validatedFields = LoginFormSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
});
const errorMessage = { message: 'Invalid login credentials.' };

// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

// 2. Query the database for the user with the given email
const user = await db.query.users.findFirst({
where: eq(users.email, validatedFields.data.email),
});

// If user is not found, return early
if (!user) {
return errorMessage;
}
// 3. Compare the user's password with the hashed password in the database
const passwordMatch = await bcrypt.compare(
validatedFields.data.password,
user.password,
);

// If the password does not match, return early
if (!passwordMatch) {
return errorMessage;
}

// 4. If login successful, create a session for the user and redirect
const userId = user.id.toString();
await createSession(userId);
redirect('/dashboard');
}

export async function logout() {
deleteSession();
redirect('/login');
}
Loading