audit: 33-agent comprehensive audit + critical fixes

Full team audit run, all reports verbatim in docs/AUDIT-2026-05-12.md
(5900+ lines, 30+ critical findings). Already-fixed this commit:
- permission-overrides PUT: self-target block + RolePermissions allow-list + cross-tenant guard
- /api/auth/resolve-identifier: rate-limit + synthetic miss-email kill enumeration
- admin email-change: rotates account.accountId + revokes sessions
- middleware: token-gated email confirm/cancel routes whitelisted
- NAV_CATALOG: 10 dead-link sweeps to existing /admin/<x> targets

Feature work landing same commit: optional username sign-in
(migration 0054), per-user permission overrides (0055) with three-state
matrix tabbed inside UserForm, user disable button, role + outcome +
stage label normalisation across the platform, admin email-change
with auto-notification template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:52:35 +02:00
parent 660553c074
commit 4b9743a594
31 changed files with 7042 additions and 81 deletions

View File

@@ -28,6 +28,7 @@ interface MeResponse {
firstName?: string | null;
lastName?: string | null;
displayName?: string | null;
username?: string | null;
};
}
@@ -35,6 +36,9 @@ export function UserSettings() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [displayName, setDisplayName] = useState('');
const [username, setUsername] = useState('');
const [originalUsername, setOriginalUsername] = useState('');
const [usernameMsg, setUsernameMsg] = useState<string | null>(null);
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [originalEmail, setOriginalEmail] = useState('');
@@ -75,6 +79,8 @@ export function UserSettings() {
setDisplayName(res.data.profile?.displayName ?? res.data.user?.name ?? '');
setEmail(res.data.user?.email ?? '');
setOriginalEmail(res.data.user?.email ?? '');
setUsername(res.data.profile?.username ?? '');
setOriginalUsername(res.data.profile?.username ?? '');
setCountry(res.data.preferences?.country ?? null);
// Fall back to the browser-detected zone when no value has been saved
// yet — first-time users land on a sensible default rather than an
@@ -149,6 +155,25 @@ export function UserSettings() {
}
}
async function saveUsername() {
if (username.trim().toLowerCase() === originalUsername.toLowerCase()) return;
setSaving('username');
setUsernameMsg(null);
try {
const next = username.trim().toLowerCase() || null;
await apiFetch('/api/v1/me', { method: 'PATCH', body: { username: next } });
setOriginalUsername(next ?? '');
setUsername(next ?? '');
setUsernameMsg(
next ? `Username updated. You can now sign in with @${next} or your email.` : 'Username cleared.',
);
} catch (err: unknown) {
setUsernameMsg(err instanceof Error ? err.message : 'Failed to save username');
} finally {
setSaving(null);
}
}
async function saveEmail() {
if (email === originalEmail) return;
setSaving('email');
@@ -328,6 +353,38 @@ export function UserSettings() {
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="settings-username">
Username <span className="text-muted-foreground">(optional)</span>
</Label>
<Input
id="settings-username"
value={username}
onChange={(e) => setUsername(e.target.value.toLowerCase())}
placeholder="yourname"
autoCapitalize="none"
spellCheck={false}
pattern="^[a-z0-9._-]{2,30}$"
/>
<div className="flex items-center gap-3">
<Button
size="sm"
variant="outline"
onClick={saveUsername}
disabled={
saving === 'username' ||
username.trim().toLowerCase() === originalUsername.toLowerCase()
}
>
{saving === 'username' ? 'Saving…' : 'Save username'}
</Button>
{usernameMsg && <span className="text-xs text-muted-foreground">{usernameMsg}</span>}
</div>
<p className="text-xs text-muted-foreground">
Optional alias you can use to sign in instead of your email. 230 lowercase
letters, digits, dot, underscore, or hyphen.
</p>
</div>
<div className="space-y-2 pt-2 border-t">
<Label htmlFor="settings-email">Email</Label>
<Input
id="settings-email"