Skip to content

Commit

Permalink
Merge pull request #1 from vercel-labs/add-basic-auth
Browse files Browse the repository at this point in the history
Add basic auth
  • Loading branch information
delbaoliveira authored Mar 25, 2024
2 parents 6766a8e + 568627e commit b3ffe78
Show file tree
Hide file tree
Showing 25 changed files with 2,061 additions and 3,245 deletions.
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

0 comments on commit b3ffe78

Please sign in to comment.