allow users to change their password

Closes #13
This commit is contained in:
Markus Brueckner 2025-01-07 22:13:06 +01:00
parent d6ad0d2ccd
commit f45deb8680
7 changed files with 153 additions and 22 deletions

View file

@ -1,7 +1,7 @@
import { config } from "$lib/configuration"; import { config } from "$lib/configuration";
import { generateRandomToken } from "$lib/randomToken"; import { generateRandomToken } from "$lib/randomToken";
import type { Email, Password, VerificationCode } from "$lib/types"; import type { Email, Password, VerificationCode } from "$lib/types";
import { hash } from "@node-rs/argon2"; import { hash, verify } from "@node-rs/argon2";
import { db } from "."; import { db } from ".";
import { usersTable } from "./schema"; import { usersTable } from "./schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@ -36,6 +36,22 @@ export async function createNewUser(email: Email, password: Password): Promise<{
} }
} }
export async function loadUser(id: number) {
const user = await db.select().from(usersTable).where(eq(usersTable.id, id)).limit(1);
if (user.length === 0) {
return null;
}
return {
id: user[0].id,
email: user[0].email as Email,
password_hash: user[0].password_hash
}
}
export async function verifyPassword(password: Password, password_hash: string) {
return await verify(password_hash, password);
}
export enum VerificationError { export enum VerificationError {
InvalidVerificationCode = 'Invalid verification code', InvalidVerificationCode = 'Invalid verification code',
VerificationCodeExpired = 'Verification code expired' VerificationCodeExpired = 'Verification code expired'

View file

@ -0,0 +1,27 @@
<script lang="ts">
type Props = {
password: string;
password_repeat: string;
};
let { password = $bindable(), password_repeat = $bindable() } = $props();
</script>
<label for="password" class="justify-self-end">Password</label>
<input
type="password"
name="password"
id="password"
class="justify-self-start"
required
bind:value={password}
/>
<label for="password_repeat" class="justify-self-end">Password (repeat)</label>
<input
type="password"
name="password_repeat"
id="password_repeat"
class="justify-self-start"
required
bind:value={password_repeat}
/>

View file

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View file

@ -6,9 +6,30 @@
import Navbar from '$lib/components/Navbar.svelte'; import Navbar from '$lib/components/Navbar.svelte';
import CheckIcon from '$lib/components/icons/CheckIcon.svelte'; import CheckIcon from '$lib/components/icons/CheckIcon.svelte';
import DuplicateIcon from '$lib/components/icons/DuplicateIcon.svelte'; import DuplicateIcon from '$lib/components/icons/DuplicateIcon.svelte';
import { page } from '$app/state';
import { toast } from '@zerodevx/svelte-toast';
import { goto } from '$app/navigation';
import ProfileIcon from '$lib/components/icons/ProfileIcon.svelte';
$effect(() => {
if (page.url.searchParams.get('pwd_updated') === 'true') {
toast.push('Password update successful', {
theme: {
'--toastProgressBackground': 'green'
},
onpop: () => {
goto('/');
}
});
}
});
</script> </script>
<Navbar title="Dashboard" /> <Navbar title="Dashboard">
<div class="w-30 flex flex-row justify-self-end">
<a href="passwordChange" title="Change password"><ProfileIcon /></a>
</div>
</Navbar>
<div class="p-4"> <div class="p-4">
<h2 class="text-2xl">Surveys you own</h2> <h2 class="text-2xl">Surveys you own</h2>

View file

@ -0,0 +1,33 @@
import type { Email, Password } from "$lib/types";
import { error, redirect, type Actions } from "@sveltejs/kit";
import { db } from "../../../db";
import { usersTable } from "../../../db/schema";
import { loadUser, verifyPassword } from "../../../db/users";
import { hash } from "@node-rs/argon2";
import { eq } from "drizzle-orm";
export const actions = {
default: async (event) => {
const formData = await event.request.formData();
const old_password = formData.get('old_password')?.toString() as Password | undefined;
const password = formData.get('password')?.toString() as Password | undefined;
if (!password || !old_password) {
error(400, 'Old and new password is required');
}
if (event.locals.userId === null) {
error(403, 'User is not logged in');
}
// load and verify the old credentials and update the password hash
const user = await loadUser(event.locals.userId);
if (!user) {
error(403, 'User does not exist');
}
if (await verifyPassword(old_password, user.password_hash)) {
await db.update(usersTable).set({ password_hash: await hash(password) }).where(eq(usersTable.id, event.locals.userId));
}
redirect(303, '/?pwd_updated=true');
}
} satisfies Actions;

View file

@ -0,0 +1,36 @@
<script>
import Navbar from '$lib/components/Navbar.svelte';
import PasswordSetterFormPart from '$lib/components/PasswordSetterFormPart.svelte';
let password = $state();
let password_repeat = $state();
</script>
<Navbar title="Update password" />
<p class="m-4 text-center">
Please provide your old password and the new password twice to update your password.
</p>
<form method="post" class="grid grid-cols-2 gap-1">
<label for="old_password" class="justify-self-end">Old password</label>
<input
type="password"
name="old_password"
id="old_password"
class="justify-self-start"
required
/>
<PasswordSetterFormPart bind:password bind:password_repeat />
<button
type="submit"
class="col-span-2 w-40 justify-self-center bg-slate-200"
disabled={password !== password_repeat || password === ''}
>
{#if password !== password_repeat}
New passwords don't match
{:else}
Update password
{/if}
</button>
</form>

View file

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import Navbar from '$lib/components/Navbar.svelte'; import Navbar from '$lib/components/Navbar.svelte';
import PasswordSetterFormPart from '$lib/components/PasswordSetterFormPart.svelte';
let password = $state(''); let password = $state();
let password_repeat = $state(''); let password_repeat = $state();
const { data } = $props(); const { data } = $props();
</script> </script>
@ -19,24 +20,7 @@
{/if} {/if}
</label> </label>
<input type="email" name="email" id="email" class="justify-self-start" required /> <input type="email" name="email" id="email" class="justify-self-start" required />
<label for="password" class="justify-self-end">Password</label> <PasswordSetterFormPart bind:password bind:password_repeat />
<input
type="password"
name="password"
id="password"
class="justify-self-start"
required
bind:value={password}
/>
<label for="password_repeat" class="justify-self-end">Password</label>
<input
type="password"
name="password_repeat"
id="password_repeat"
class="justify-self-start"
required
bind:value={password_repeat}
/>
<button <button
type="submit" type="submit"
class="col-span-2 w-40 justify-self-center bg-slate-200" class="col-span-2 w-40 justify-self-center bg-slate-200"