Add self-service password change, forgot-password flow, and fix admin UI bugs

Users can change their own password from the profile modal, and a
"forgot password" link sends a 15-minute expiring reset link (same
hashed-token pattern as site invites). The forgot-password response is
always generic so it never reveals which emails are registered.

Also fixes two admin-page display bugs found while testing: table row
divider lines that didn't line up across a row (the actions column had
`display: flex` on the <td> itself, breaking it out of normal table-cell
layout -- moved to a child <div>), and the pending-invites list floating
with no visual grouping (now boxed with a label and per-status badges).
This commit is contained in:
2026-08-14 21:06:17 -06:00
parent 8e3b6a16bd
commit fc96e85014
18 changed files with 836 additions and 38 deletions
+25
View File
@@ -31,6 +31,31 @@ export function removeAvatar(): Promise<User> {
return apiFetch<User>('/api/auth/me/avatar', { method: 'DELETE' })
}
export function changePassword(currentPassword: string, newPassword: string): Promise<void> {
return apiFetch<void>('/api/auth/password', {
method: 'PATCH',
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
})
}
export function requestPasswordReset(email: string): Promise<void> {
return apiFetch<void>('/api/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email }),
})
}
export function validateResetToken(token: string): Promise<void> {
return apiFetch<void>(`/api/auth/reset-password/validate?token=${encodeURIComponent(token)}`)
}
export function completePasswordReset(token: string, newPassword: string): Promise<User> {
return apiFetch<User>('/api/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, new_password: newPassword }),
})
}
// Not apiFetch: that wrapper always sets Content-Type: application/json,
// which would stomp the multipart boundary the browser needs to set itself
// for a file upload. Mirrors api/rooms.ts's uploadRoomImage.