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
+70
View File
@@ -0,0 +1,70 @@
import { useState, type FormEvent } from 'react'
import { Link, Navigate } from 'react-router-dom'
import { requestPasswordReset } from '../api/auth'
import { useAuth } from '../context/AuthContext'
import logo from '../assets/logo.png'
import './LoginPage.css'
export function ForgotPasswordPage() {
const { user } = useAuth()
const [email, setEmail] = useState('')
const [submitting, setSubmitting] = useState(false)
const [sent, setSent] = useState(false)
if (user) return <Navigate to="/rooms" replace />
async function handleSubmit(e: FormEvent) {
e.preventDefault()
setSubmitting(true)
try {
await requestPasswordReset(email)
} catch {
// Fall through to the generic message regardless -- the request
// itself never reveals whether the email is registered.
} finally {
setSubmitting(false)
setSent(true)
}
}
return (
<div className="login-screen">
<div className="login-card">
<div className="login-brand">
<img src={logo} alt="" />
<span>KeepItTalking</span>
</div>
{sent ? (
<p className="login-copy">
If an account exists for that email, a password reset link is on its way. The link
expires in 15 minutes.
</p>
) : (
<>
<p className="login-copy">Enter your account email and we'll send a reset link.</p>
<form className="login-form" onSubmit={handleSubmit}>
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoFocus
/>
</label>
<button type="submit" className="btn-primary" disabled={submitting}>
{submitting ? 'Sending' : 'Send reset link'}
</button>
</form>
</>
)}
<Link to="/login" className="login-link">
Back to log in
</Link>
</div>
</div>
)
}