add password reset via admin-generated one-time code
db.py: reset_codes table (code, handle, created_by, created_at,
expires_at, used_at); create_reset_code() invalidates any prior unused
code for the same handle; use_reset_code() validates handle match,
expiry (24 h), and single-use; change_password() updates the hash.
server.py: POST /api/admin/users/{handle}/reset-password-code (admin)
returns a code; POST /api/auth/reset-password (public) validates the
code + handle and sets the new password.
Admin page: "Reset pwd" button per user — shows the code inline on
click (monospace, click-to-copy).
/reset-password/ page: handle + code + new password form.
Login page: "Forgot password?" link.
This commit is contained in:
@@ -122,6 +122,11 @@ import Base from '../../layouts/Base.astro';
|
||||
data-handle="${u.handle}"
|
||||
title="Re-run merge_all and trigger a site rebuild"
|
||||
>Rebuild</button>
|
||||
<button
|
||||
class="pwreset-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||
data-handle="${u.handle}"
|
||||
title="Generate a one-time password reset code for this user"
|
||||
>Reset pwd</button>
|
||||
<button
|
||||
class="delete-btn text-xs px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-red-900 hover:text-red-300 text-zinc-400 transition-colors"
|
||||
data-handle="${u.handle}"
|
||||
@@ -161,6 +166,35 @@ import Base from '../../layouts/Base.astro';
|
||||
});
|
||||
});
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.pwreset-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const h = btn.dataset.handle!;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const r = await fetch(`/api/admin/users/${h}/reset-password-code`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
const d = await r.json();
|
||||
if (r.ok) {
|
||||
btn.textContent = d.code;
|
||||
btn.title = `Code for ${h} — valid 24 h. Click to copy.`;
|
||||
btn.classList.add('text-yellow-300', 'font-mono');
|
||||
btn.addEventListener('click', () => navigator.clipboard.writeText(d.code), { once: true });
|
||||
} else {
|
||||
btn.textContent = 'Error';
|
||||
btn.classList.add('text-red-400');
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch {
|
||||
btn.textContent = 'Error';
|
||||
btn.classList.add('text-red-400');
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tbodyEl.querySelectorAll<HTMLButtonElement>('.delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
pendingHandle = btn.dataset.handle!;
|
||||
|
||||
Reference in New Issue
Block a user