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:
@@ -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. 2–30 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"
|
||||
|
||||
Reference in New Issue
Block a user