parent
d6ad0d2ccd
commit
f45deb8680
7 changed files with 153 additions and 22 deletions
|
@ -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'
|
||||||
|
|
27
src/lib/components/PasswordSetterFormPart.svelte
Normal file
27
src/lib/components/PasswordSetterFormPart.svelte
Normal 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}
|
||||||
|
/>
|
14
src/lib/components/icons/ProfileIcon.svelte
Normal file
14
src/lib/components/icons/ProfileIcon.svelte
Normal 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 |
|
@ -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>
|
||||||
|
|
33
src/routes/(app)/passwordChange/+page.server.ts
Normal file
33
src/routes/(app)/passwordChange/+page.server.ts
Normal 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;
|
36
src/routes/(app)/passwordChange/+page.svelte
Normal file
36
src/routes/(app)/passwordChange/+page.svelte
Normal 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>
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue