Spaces:
Sleeping
Sleeping
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>
|