This commit is contained in:
@@ -7,7 +7,12 @@ export const POST: RequestHandler = async ({ request, cookies, locals, url, getC
|
||||
consumeAuthAttempt(getClientAddress(), user.id, 'password');
|
||||
const body = await authBody(request);
|
||||
return sessionResponse(
|
||||
await changePassword(user.id, body.currentPassword, body.newPassword),
|
||||
await changePassword(
|
||||
user.id,
|
||||
body.currentPassword,
|
||||
body.newPassword,
|
||||
locals.sessionHash ?? ''
|
||||
),
|
||||
cookies,
|
||||
url
|
||||
);
|
||||
|
||||
@@ -2,10 +2,15 @@ import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { createUser, listUsers } from '$lib/server/auth/index.js';
|
||||
import { authBody, authError, requireUser } from '$lib/server/auth/http.js';
|
||||
export const GET: RequestHandler = () => json({ users: listUsers() });
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
export const POST: RequestHandler = async ({ request, locals, getClientAddress }) => {
|
||||
try {
|
||||
return json(
|
||||
{ user: await createUser(requireUser(locals.user).id, await authBody(request)) },
|
||||
{
|
||||
user: await createUser(requireUser(locals.user).id, await authBody(request), {
|
||||
tokenHash: locals.sessionHash ?? '',
|
||||
clientAddress: getClientAddress()
|
||||
})
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (cause) {
|
||||
|
||||
@@ -2,10 +2,18 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { updateUser } from '$lib/server/auth/index.js';
|
||||
import { authBody, authError, requireUser } from '$lib/server/auth/http.js';
|
||||
export const PATCH: RequestHandler = async ({ request, params, locals }) => {
|
||||
export const PATCH: RequestHandler = async ({ request, params, locals, getClientAddress }) => {
|
||||
try {
|
||||
return json({
|
||||
user: await updateUser(requireUser(locals.user).id, params.id, await authBody(request))
|
||||
user: await updateUser(
|
||||
requireUser(locals.user).id,
|
||||
params.id,
|
||||
await authBody(request),
|
||||
{
|
||||
tokenHash: locals.sessionHash ?? '',
|
||||
clientAddress: getClientAddress()
|
||||
}
|
||||
)
|
||||
});
|
||||
} catch (cause) {
|
||||
return authError(cause);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
let username = $state('');
|
||||
let password = $state('');
|
||||
let rememberMe = $state(false);
|
||||
let busy = $state(false);
|
||||
let error = $state('');
|
||||
function destination(): string {
|
||||
@@ -28,7 +29,7 @@
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
body: JSON.stringify({ username, password, rememberMe })
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not sign in. Try again.');
|
||||
@@ -76,6 +77,20 @@
|
||||
bind:value={password}
|
||||
/></Field
|
||||
>
|
||||
<div class="remember-device">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="rememberMe"
|
||||
bind:checked={rememberMe}
|
||||
aria-describedby="remember-help"
|
||||
/>
|
||||
Keep me signed in on this device
|
||||
</label>
|
||||
<p id="remember-help">
|
||||
Stay signed in for 30 days between visits. Use only on a device you trust.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="submit" variant="primary" loading={busy} disabled={busy}>Sign in</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -111,6 +126,25 @@
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.remember-device label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.remember-device input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--accent-primary);
|
||||
}
|
||||
.remember-device p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.help {
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { readSessionHash } from '$lib/server/auth/index.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = ({ locals }) => ({
|
||||
rememberedSession: !!(locals.sessionHash && readSessionHash(locals.sessionHash)?.remembered)
|
||||
});
|
||||
@@ -23,6 +23,7 @@
|
||||
let username = $state('');
|
||||
let displayName = $state('');
|
||||
let password = $state('');
|
||||
let currentPassword = $state('');
|
||||
let role = $state<UserRole>('viewer');
|
||||
let enabled = $state(true);
|
||||
let showPassword = $state(false);
|
||||
@@ -62,7 +63,11 @@
|
||||
];
|
||||
const roleDescription = $derived(roles.find((item) => item.value === role)?.description);
|
||||
const administrator = $derived($page.data.user?.role === 'admin');
|
||||
const rememberedSession = $derived($page.data.rememberedSession);
|
||||
const canViewUsers = $derived(administrator || $page.data.user?.role === 'super_viewer');
|
||||
$effect(() => {
|
||||
if (!createOpen && !editOpen) currentPassword = '';
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
@@ -118,6 +123,7 @@
|
||||
});
|
||||
function openCreate() {
|
||||
if (!administrator) return;
|
||||
currentPassword = '';
|
||||
username = '';
|
||||
displayName = '';
|
||||
password = '';
|
||||
@@ -130,6 +136,7 @@
|
||||
}
|
||||
function openEdit(user: AuthUser) {
|
||||
if (!administrator) return;
|
||||
currentPassword = '';
|
||||
editing = user;
|
||||
username = user.username;
|
||||
displayName = user.displayName;
|
||||
@@ -151,7 +158,7 @@
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, displayName, password, role })
|
||||
body: JSON.stringify({ username, displayName, password, role, currentPassword })
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not create this user.');
|
||||
@@ -162,6 +169,7 @@
|
||||
} catch (cause) {
|
||||
formError = (cause as Error).message;
|
||||
} finally {
|
||||
currentPassword = '';
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
@@ -183,6 +191,7 @@
|
||||
displayName,
|
||||
role,
|
||||
disabled: !enabled,
|
||||
currentPassword,
|
||||
...(password ? { password } : {})
|
||||
})
|
||||
});
|
||||
@@ -200,6 +209,7 @@
|
||||
} catch (cause) {
|
||||
formError = (cause as Error).message;
|
||||
} finally {
|
||||
currentPassword = '';
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
@@ -370,6 +380,26 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#snippet accountPassword(id: string)}
|
||||
{#if rememberedSession}
|
||||
<Field forId={id} label="Your current password">
|
||||
<Input
|
||||
{id}
|
||||
name="currentPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
maxlength={128}
|
||||
bind:value={currentPassword}
|
||||
aria-describedby={`${id}-help`}
|
||||
/>
|
||||
</Field>
|
||||
<p class="hint" id={`${id}-help`}>
|
||||
Confirm your password to make account changes on this remembered device.
|
||||
</p>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if administrator}<Dialog
|
||||
bind:open={createOpen}
|
||||
title="Add user"
|
||||
@@ -439,6 +469,7 @@
|
||||
<label class="checkbox"
|
||||
><input type="checkbox" bind:checked={showPassword} />Show temporary password</label
|
||||
>
|
||||
{@render accountPassword('create-current-password')}
|
||||
<div class="form-actions">
|
||||
<Button
|
||||
disabled={busy}
|
||||
@@ -518,6 +549,7 @@
|
||||
><input type="checkbox" bind:checked={showPassword} />Show temporary
|
||||
password</label
|
||||
>{/if}
|
||||
{@render accountPassword('edit-current-password')}
|
||||
<div class="form-actions">
|
||||
<Button
|
||||
disabled={busy}
|
||||
|
||||
Reference in New Issue
Block a user