assafvayner HF Staff commited on
Commit
b721171
·
1 Parent(s): 59d41c0

feat(ui): add profile page and forced password change

Browse files
src/routes/me/+page.server.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { UserValidationError } from '$lib/server/users/repository';
2
+ import { fail } from '@sveltejs/kit';
3
+ import type { Actions } from './$types';
4
+
5
+ export const actions: Actions = {
6
+ rename: async ({ request, locals }) => {
7
+ const form = await request.formData();
8
+ const displayName = String(form.get('displayName') ?? '');
9
+ try {
10
+ await locals.app.users.rename(locals.user!.id, displayName);
11
+ return { renamed: true };
12
+ } catch (e) {
13
+ if (e instanceof UserValidationError) return fail(400, { error: e.message, displayName });
14
+ throw e;
15
+ }
16
+ }
17
+ };
src/routes/me/+page.svelte ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { enhance } from '$app/forms';
3
+ import FormError from '$lib/components/FormError.svelte';
4
+ import type { PageProps } from './$types';
5
+
6
+ let { data, form }: PageProps = $props();
7
+ </script>
8
+
9
+ <main class="space-y-6 p-4">
10
+ <h1 class="text-2xl font-bold">Me</h1>
11
+ <p class="text-gray-600">
12
+ Logged in as <span class="font-mono">{data.user?.username}</span>{data.user?.role === 'admin'
13
+ ? ' (admin)'
14
+ : ''}
15
+ </p>
16
+
17
+ <form method="POST" action="?/rename" use:enhance class="space-y-2">
18
+ <label for="displayName">Display name</label>
19
+ <input
20
+ id="displayName"
21
+ name="displayName"
22
+ required
23
+ maxlength="120"
24
+ value={form?.displayName ?? data.user?.displayName ?? ''}
25
+ />
26
+ <FormError message={form?.error} />
27
+ {#if form?.renamed}<p class="text-sm text-green-700">Saved.</p>{/if}
28
+ <button type="submit" class="rounded-lg bg-green-700 px-4 py-2 font-semibold text-white"
29
+ >Save</button
30
+ >
31
+ </form>
32
+
33
+ <a href="/me/password" class="block rounded-lg border border-gray-300 px-4 py-3 text-center"
34
+ >Change password</a
35
+ >
36
+
37
+ <form method="POST" action="/logout">
38
+ <button type="submit" class="w-full rounded-lg border border-red-300 px-4 py-3 text-red-700"
39
+ >Log out</button
40
+ >
41
+ </form>
42
+ </main>
src/routes/me/password/+page.server.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dev } from '$app/environment';
2
+ import { verifyPassword } from '$lib/server/auth/password';
3
+ import { createSessionToken, SESSION_COOKIE, SESSION_TTL_SECONDS } from '$lib/server/auth/session';
4
+ import { UserValidationError } from '$lib/server/users/repository';
5
+ import { fail, redirect } from '@sveltejs/kit';
6
+ import type { Actions, PageServerLoad } from './$types';
7
+
8
+ export const load: PageServerLoad = ({ locals }) => ({ forced: locals.user!.mustChangePassword });
9
+
10
+ export const actions: Actions = {
11
+ default: async ({ request, locals, cookies }) => {
12
+ const user = locals.user!;
13
+ const form = await request.formData();
14
+ const current = String(form.get('current') ?? '');
15
+ const next = String(form.get('next') ?? '');
16
+ const confirm = String(form.get('confirm') ?? '');
17
+
18
+ if (!(await verifyPassword(current, user.passwordHash)))
19
+ return fail(400, { error: 'Current password is incorrect' });
20
+ if (next === current) return fail(400, { error: 'New password must differ from the current one' });
21
+ if (next !== confirm) return fail(400, { error: 'New passwords do not match' });
22
+
23
+ try {
24
+ const updated = await locals.app.users.setPassword(user.id, next, { mustChange: false });
25
+ const token = createSessionToken(
26
+ { uid: updated.id, sv: updated.sessionVersion },
27
+ locals.app.config.sessionSecret
28
+ );
29
+ cookies.set(SESSION_COOKIE, token, {
30
+ path: '/',
31
+ httpOnly: true,
32
+ sameSite: 'lax',
33
+ secure: !dev,
34
+ maxAge: SESSION_TTL_SECONDS
35
+ });
36
+ } catch (e) {
37
+ if (e instanceof UserValidationError) return fail(400, { error: e.message });
38
+ throw e;
39
+ }
40
+ redirect(303, '/');
41
+ }
42
+ };
src/routes/me/password/+page.svelte ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { enhance } from '$app/forms';
3
+ import FormError from '$lib/components/FormError.svelte';
4
+ import type { PageProps } from './$types';
5
+
6
+ let { data, form }: PageProps = $props();
7
+ </script>
8
+
9
+ <main class="space-y-6 p-4">
10
+ <h1 class="text-2xl font-bold">Change password</h1>
11
+ {#if data.forced}
12
+ <p class="rounded-lg bg-amber-100 p-3 text-sm text-amber-900">
13
+ You must choose a new password before continuing.
14
+ </p>
15
+ {/if}
16
+ <form method="POST" use:enhance class="space-y-4">
17
+ <div>
18
+ <label for="current">Current password</label>
19
+ <input id="current" name="current" type="password" autocomplete="current-password" required />
20
+ </div>
21
+ <div>
22
+ <label for="next">New password (8+ characters)</label>
23
+ <input id="next" name="next" type="password" autocomplete="new-password" minlength="8" required />
24
+ </div>
25
+ <div>
26
+ <label for="confirm">Confirm new password</label>
27
+ <input
28
+ id="confirm"
29
+ name="confirm"
30
+ type="password"
31
+ autocomplete="new-password"
32
+ minlength="8"
33
+ required
34
+ />
35
+ </div>
36
+ <FormError message={form?.error} />
37
+ <button type="submit" class="w-full rounded-lg bg-green-700 py-3 font-semibold text-white"
38
+ >Save password</button
39
+ >
40
+ </form>
41
+ </main>