Password change as modal + admin can reset other user passwords
Some checks failed
Validate / Hassfest (push) Has been cancelled
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user