Kyle Pope f2050efe2d Redesign Settings page with tab-based layout
Replace 895-line monolith with 5 focused tab components (Profile,
Appearance, Social, Security, Integrations) mirroring AdminPortal's
tab pattern. URL deep linking via ?tab= search param. Conditional
rendering prevents unmounted tabs from firing API calls.

Reviewed by senior-code-reviewer, senior-ui-designer, and
security-penetration-tester agents — all findings actioned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:58:28 +08:00

94 lines
3.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import TotpSetupSection from './TotpSetupSection';
import type { Settings } from '@/types';
interface SecurityTabProps {
settings: Settings | undefined;
updateSettings: (updates: Partial<Settings>) => Promise<Settings>;
isUpdating: boolean;
}
export default function SecurityTab({ settings, updateSettings, isUpdating }: SecurityTabProps) {
const [autoLockEnabled, setAutoLockEnabled] = useState(settings?.auto_lock_enabled ?? false);
const [autoLockMinutes, setAutoLockMinutes] = useState<number | string>(settings?.auto_lock_minutes ?? 5);
useEffect(() => {
if (settings) {
setAutoLockEnabled(settings.auto_lock_enabled);
setAutoLockMinutes(settings.auto_lock_minutes ?? 5);
}
}, [settings?.id]);
const handleAutoLockToggle = async (checked: boolean) => {
const previous = autoLockEnabled;
setAutoLockEnabled(checked);
try {
await updateSettings({ auto_lock_enabled: checked });
toast.success(checked ? 'Auto-lock enabled' : 'Auto-lock disabled');
} catch {
setAutoLockEnabled(previous);
toast.error('Failed to update auto-lock setting');
}
};
const handleAutoLockMinutesSave = async () => {
const raw = typeof autoLockMinutes === 'string' ? parseInt(autoLockMinutes) : autoLockMinutes;
const clamped = Math.max(1, Math.min(60, isNaN(raw) ? 5 : raw));
setAutoLockMinutes(clamped);
if (clamped === settings?.auto_lock_minutes) return;
try {
await updateSettings({ auto_lock_minutes: clamped });
toast.success(`Auto-lock timeout set to ${clamped} minutes`);
} catch {
setAutoLockMinutes(settings?.auto_lock_minutes ?? 5);
toast.error('Failed to update auto-lock timeout');
}
};
return (
<div className="space-y-6">
{/* Auto-lock */}
<Card>
<CardContent className="pt-6 space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>Auto-lock</Label>
<p className="text-sm text-muted-foreground">
Automatically lock the screen after idle time
</p>
</div>
<Switch
checked={autoLockEnabled}
onCheckedChange={handleAutoLockToggle}
/>
</div>
<div className="flex items-center gap-3">
<Label htmlFor="auto_lock_minutes" className="shrink-0">Lock after</Label>
<Input
id="auto_lock_minutes"
type="number"
min="1"
max="60"
value={autoLockMinutes}
onChange={(e) => setAutoLockMinutes(e.target.value === '' ? '' : parseInt(e.target.value) || '')}
onBlur={handleAutoLockMinutesSave}
onKeyDown={(e) => { if (e.key === 'Enter') handleAutoLockMinutesSave(); }}
className="w-20"
disabled={!autoLockEnabled || isUpdating}
/>
<span className="text-sm text-muted-foreground shrink-0">minutes</span>
</div>
</CardContent>
</Card>
{/* Password + TOTP */}
<TotpSetupSection bare />
</div>
);
}