Private
Public Access
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:
@@ -1,5 +1,5 @@
|
||||
import { useRef, useState, type ChangeEvent, type FormEvent } from 'react'
|
||||
import { removeAvatar, updateProfile, uploadAvatar } from '../api/auth'
|
||||
import { changePassword, removeAvatar, updateProfile, uploadAvatar } from '../api/auth'
|
||||
import { ApiError } from '../api/client'
|
||||
import { getUserAvatarUrl } from '../api/users'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
@@ -19,6 +19,13 @@ export function ProfileModal({ onClose }: ProfileModalProps) {
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [passwordError, setPasswordError] = useState<string | null>(null)
|
||||
const [passwordSuccess, setPasswordSuccess] = useState(false)
|
||||
const [savingPassword, setSavingPassword] = useState(false)
|
||||
|
||||
if (!user) return null
|
||||
|
||||
async function handleSaveName(e: FormEvent) {
|
||||
@@ -61,6 +68,28 @@ export function ProfileModal({ onClose }: ProfileModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePassword(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setPasswordError(null)
|
||||
setPasswordSuccess(false)
|
||||
if (newPassword !== confirmPassword) {
|
||||
setPasswordError("New passwords don't match")
|
||||
return
|
||||
}
|
||||
setSavingPassword(true)
|
||||
try {
|
||||
await changePassword(currentPassword, newPassword)
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
setConfirmPassword('')
|
||||
setPasswordSuccess(true)
|
||||
} catch (err) {
|
||||
setPasswordError(err instanceof ApiError ? err.message : String(err))
|
||||
} finally {
|
||||
setSavingPassword(false)
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = user.avatar_filename ? getUserAvatarUrl(user.id, user.avatar_filename) : null
|
||||
|
||||
return (
|
||||
@@ -123,6 +152,46 @@ export function ProfileModal({ onClose }: ProfileModalProps) {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr className="modal-divider" />
|
||||
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="modal-field-label">Change password</div>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
placeholder="Current password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="New password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
/>
|
||||
{passwordError && <p className="modal-error">{passwordError}</p>}
|
||||
{passwordSuccess && <p className="modal-success">Password updated.</p>}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={savingPassword || !currentPassword || !newPassword || !confirmPassword}
|
||||
>
|
||||
{savingPassword ? 'Saving…' : 'Update password'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user