Password change as modal + admin can reset other user passwords
Some checks failed
Validate / Hassfest (push) Has been cancelled

- New Modal.svelte component: overlay with backdrop click to close,
  title bar, reusable via children snippet
- Layout: password change moved from inline sidebar form to modal
  dialog. Clean UX with current + new password fields.
- Users page: 🔑 button per user opens modal for admin to set a
  new password (no current password required for admin reset)
- Backend: PUT /api/users/{id}/password (admin only, min 6 chars)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 17:32:03 +03:00
parent 0200b9929f
commit 7b7ef5fec1
4 changed files with 112 additions and 16 deletions

View File

@@ -6,6 +6,7 @@
import PageHeader from '$lib/components/PageHeader.svelte';
import Card from '$lib/components/Card.svelte';
import Loading from '$lib/components/Loading.svelte';
import Modal from '$lib/components/Modal.svelte';
const auth = getAuth();
let users = $state<any[]>([]);
@@ -14,6 +15,12 @@
let error = $state('');
let loaded = $state(false);
// Admin reset password
let resetUserId = $state<number | null>(null);
let resetUsername = $state('');
let resetPassword = $state('');
let resetMsg = $state('');
onMount(load);
async function load() { try { users = await api('/users'); } catch {} finally { loaded = true; } }
@@ -26,6 +33,17 @@
if (!confirm(t('users.confirmDelete'))) return;
try { await api(`/users/${id}`, { method: 'DELETE' }); await load(); } catch (err: any) { alert(err.message); }
}
function openResetPassword(user: any) {
resetUserId = user.id; resetUsername = user.username; resetPassword = ''; resetMsg = '';
}
async function resetUserPassword(e: SubmitEvent) {
e.preventDefault(); resetMsg = '';
try {
await api(`/users/${resetUserId}/password`, { method: 'PUT', body: JSON.stringify({ new_password: resetPassword }) });
resetMsg = t('common.passwordChanged');
setTimeout(() => { resetUserId = null; resetMsg = ''; }, 2000);
} catch (err: any) { resetMsg = err.message; }
}
</script>
<PageHeader title={t('users.title')} description={t('users.description')}>
@@ -69,12 +87,32 @@
<p class="font-medium">{user.username}</p>
<p class="text-sm text-[var(--color-muted-foreground)]">{user.role} · {t('users.joined')} {new Date(user.created_at).toLocaleDateString()}</p>
</div>
{#if user.id !== auth.user?.id}
<button onclick={() => remove(user.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('users.delete')}</button>
{/if}
<div class="flex items-center gap-3">
{#if user.id !== auth.user?.id}
<button onclick={() => openResetPassword(user)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">🔑</button>
<button onclick={() => remove(user.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('users.delete')}</button>
{/if}
</div>
</div>
</Card>
{/each}
</div>
{/if}
<!-- Admin reset password modal -->
<Modal open={resetUserId !== null} title="{t('common.changePassword')}: {resetUsername}" onclose={() => { resetUserId = null; resetMsg = ''; }}>
<form onsubmit={resetUserPassword} class="space-y-3">
<div>
<label for="reset-pwd" class="block text-sm font-medium mb-1">{t('common.newPassword')}</label>
<input id="reset-pwd" type="password" bind:value={resetPassword} required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div>
{#if resetMsg}
<p class="text-sm {resetMsg.includes(t('common.passwordChanged')) ? 'text-[var(--color-success-fg)]' : 'text-[var(--color-error-fg)]'}">{resetMsg}</p>
{/if}
<button type="submit" class="w-full py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
{t('common.save')}
</button>
</form>
</Modal>