-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from vercel-labs/add-basic-auth
Add basic auth
- Loading branch information
Showing
25 changed files
with
2,061 additions
and
3,245 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} |
Oops, something went wrong.