mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Adding password reset tokens and API routes for creating and verifying token.
This commit is contained in:
parent
8185bb76f6
commit
f3cb74ac7a
9 changed files with 175 additions and 62 deletions
19
src/lib/server/auth-utils.ts
Normal file
19
src/lib/server/auth-utils.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import db from "$lib/drizzle";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { password_reset_tokens } from "../../schema";
|
||||
import { generateId } from "lucia";
|
||||
import { TimeSpan, createDate } from "oslo";
|
||||
|
||||
export async function createPasswordResetToken(userId: string): Promise<string> {
|
||||
// optionally invalidate all existing tokens
|
||||
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId));
|
||||
const tokenId = generateId(40);
|
||||
await db
|
||||
.insert(password_reset_tokens)
|
||||
.values({
|
||||
id: tokenId,
|
||||
user_id: userId,
|
||||
expires_at: createDate(new TimeSpan(2, "h"))
|
||||
});
|
||||
return tokenId;
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ export const changeUserPasswordSchema = z
|
|||
refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
|
||||
|
||||
export const updateUserPasswordSchema = userSchema
|
||||
.pick({ password: true, confirm_password: true })
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { zodClient } from 'sveltekit-superforms/adapters';
|
||||
import { superForm } from 'sveltekit-superforms/client';
|
||||
import * as flashModule from 'sveltekit-flash-message/client';
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
import { changeUserPasswordSchema } from '$lib/validations/account';
|
||||
import { Label } from '$components/ui/label';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { Button } from '$components/ui/button';
|
||||
import { AlertTriangle } from 'lucide-svelte';
|
||||
import * as Alert from "$lib/components/ui/alert";
|
||||
import * as Form from '$lib/components/ui/form';
|
||||
import { Input } from '$components/ui/input';
|
||||
import { changeUserPasswordSchema } from '$lib/validations/account';
|
||||
|
||||
export let data;
|
||||
|
||||
const { form, errors, enhance, delayed, message } = superForm(data.form, {
|
||||
const form = superForm(data.form, {
|
||||
taintedMessage: null,
|
||||
validators: zodClient(changeUserPasswordSchema),
|
||||
delayMs: 500,
|
||||
multipleSubmits: 'prevent',
|
||||
syncFlashMessage: true,
|
||||
flashMessage: {
|
||||
module: flashModule,
|
||||
onError: ({ result }) => {
|
||||
console.log('result', result);
|
||||
const errorMessage = result.error.message
|
||||
message.set({ type: 'error', message: errorMessage });
|
||||
}
|
||||
}
|
||||
multipleSubmits: 'prevent'
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<!--<SuperDebug data={$form} />-->
|
||||
<h3>Change Password</h3>
|
||||
<hr class="!border-t-2 mt-2 mb-6" />
|
||||
<Alert.Root variant="destructive">
|
||||
|
|
@ -38,39 +29,28 @@
|
|||
Changing your password will log you out of all devices.
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{#if $message}
|
||||
<aside class="alert variant-filled-success mt-6">
|
||||
<!-- Message -->
|
||||
<div class="alert-message">
|
||||
<p>{$message}</p>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
<div class="mt-6">
|
||||
<Label for="current_password">Current Password</Label>
|
||||
<Input type="password" id="current_password" name="current_password" placeholder="Current Password" autocomplete="password" data-invalid={$errors.current_password} bind:value={$form.current_password} />
|
||||
{#if $errors.current_password}
|
||||
<small>{$errors.current_password}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6 grid">
|
||||
<Label for="password">New Password</Label>
|
||||
<Input type="password" id="password" name="password" placeholder="Password" autocomplete="given-name" data-invalid={$errors.password} bind:value={$form.password} />
|
||||
{#if $errors.password}
|
||||
<small>{$errors.password}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<Label for="confirm_password">Confirm New Password</Label>
|
||||
<Input type="password" id="confirm_password" name="confirm_password" placeholder="Confirm Password" autocomplete="family-name" data-invalid={$errors.confirm_password} bind:value={$form.confirm_password} />
|
||||
{#if $errors.confirm_password}
|
||||
<small>{$errors.confirm_password}</small>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<Button type="submit" class="w-full">Change Password</Button>
|
||||
</div>
|
||||
<Form.Field {form} name="current_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="current_password">Current Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.current_password} />
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="password">New Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.password} />
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="confirm_password">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label for="confirm_password">Confirm New Password</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.confirm_password} />
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Button>Submit</Form.Button>
|
||||
</form>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
<h1>Waitlist</h1>
|
||||
<h1>Waitlist</h1>
|
||||
|
||||
<h2>Waitlist coming soon!</h2>
|
||||
|
|
@ -11,14 +11,13 @@ import { collections, users, wishlists } from '../../../schema';
|
|||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const form = await superValidate(event, zod(signInSchema));
|
||||
|
||||
console.log('login load event', event);
|
||||
if (event.locals.user) {
|
||||
const message = { type: 'success', message: 'You are already signed in' } as const;
|
||||
throw redirect('/', message, event);
|
||||
}
|
||||
|
||||
const form = await superValidate(event, zod(signInSchema));
|
||||
|
||||
return {
|
||||
form
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { fail, error, type Actions, redirect } from '@sveltejs/kit';
|
||||
import { fail, error, type Actions } from '@sveltejs/kit';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { setError, superValidate } from 'sveltekit-superforms/server';
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { lucia } from '$lib/server/auth';
|
||||
import { signUpSchema } from '$lib/validations/auth';
|
||||
|
|
@ -22,11 +23,13 @@ const signUpDefaults = {
|
|||
};
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
console.log('sign up load event', event);
|
||||
// const session = await event.locals.auth.validate();
|
||||
// if (session) {
|
||||
// throw redirect(302, '/');
|
||||
// }
|
||||
// redirect(302, '/waitlist', { type: 'error', message: 'Sign-up not yet available. Please add your email to the waitlist!' }, event);
|
||||
|
||||
if (event.locals.user) {
|
||||
const message = { type: 'success', message: 'You are already signed in' } as const;
|
||||
throw redirect('/', message, event);
|
||||
}
|
||||
|
||||
return {
|
||||
form: await superValidate(zod(signUpSchema), {
|
||||
defaults: signUpDefaults
|
||||
|
|
@ -36,6 +39,7 @@ export const load: PageServerLoad = async (event) => {
|
|||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
// fail(401, { message: 'Sign-up not yet available. Please add your email to the waitlist!' });
|
||||
const form = await superValidate(event, zod(signUpSchema));
|
||||
if (!form.valid) {
|
||||
form.data.password = '';
|
||||
|
|
|
|||
32
src/routes/api/auth/reset-password/+server.ts
Normal file
32
src/routes/api/auth/reset-password/+server.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import db from '$lib/drizzle.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { users } from '../../../../schema.js';
|
||||
import { createPasswordResetToken } from '$lib/server/auth-utils.js';
|
||||
import { PUBLIC_SITE_URL } from '$env/static/public';
|
||||
|
||||
export async function POST({ locals, request }) {
|
||||
const { email }: { email: string } = await request.json();
|
||||
|
||||
if (!locals.user) {
|
||||
error(401, { message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.email, email)
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
error(200, { message: 'Email sent! Please check your email for a link to reset your password.' });
|
||||
}
|
||||
|
||||
const verificationToken = await createPasswordResetToken(user.id);
|
||||
const verificationLink = PUBLIC_SITE_URL + verificationToken;
|
||||
|
||||
// TODO: send email
|
||||
console.log('Verification link: ' + verificationLink);
|
||||
|
||||
return new Response(null, {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
52
src/routes/api/auth/reset-password/[token]/+server.ts
Normal file
52
src/routes/api/auth/reset-password/[token]/+server.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import db from '$lib/drizzle.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { password_reset_tokens, users } from '../../../../../schema.js';
|
||||
import { isWithinExpirationDate } from 'oslo';
|
||||
import { lucia } from '$lib/server/auth.js';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const { password } = await request.json();
|
||||
|
||||
if (typeof password !== 'string' || password.length < 8) {
|
||||
return new Response(null, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
const verificationToken = params.token;
|
||||
|
||||
const token = await db.query.password_reset_tokens.findFirst({
|
||||
where: eq(password_reset_tokens.id, verificationToken)
|
||||
});
|
||||
if (!token) {
|
||||
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.id, verificationToken));
|
||||
return new Response(null, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
if (!token?.expires_at || !isWithinExpirationDate(token.expires_at)) {
|
||||
return new Response(null, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
await lucia.invalidateUserSessions(token.user_id);
|
||||
const hashPassword = await new Argon2id().hash(password);
|
||||
await db
|
||||
.update(users)
|
||||
.set({ hashed_password: hashPassword })
|
||||
.where(eq(users.id, token.user_id));
|
||||
|
||||
const session = await lucia.createSession(token.user_id, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: "/",
|
||||
"Set-Cookie": sessionCookie.serialize()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -138,6 +138,29 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
|
|||
|
||||
export type UserRoles = InferSelectModel<typeof user_roles>;
|
||||
|
||||
export const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||
id: varchar('id', {
|
||||
length: 255
|
||||
})
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
user_id: varchar('user_id', {
|
||||
length: 255
|
||||
})
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
expires_at: timestamp('expires_at', {
|
||||
withTimezone: true,
|
||||
mode: 'date',
|
||||
precision: 6
|
||||
}),
|
||||
created_at: timestamp('created_at', {
|
||||
withTimezone: true,
|
||||
mode: 'date',
|
||||
precision: 6
|
||||
}).default(sql`now()`)
|
||||
});
|
||||
|
||||
export const collections = pgTable('collections', {
|
||||
id: varchar('id', {
|
||||
length: 255
|
||||
|
|
|
|||
Loading…
Reference in a new issue