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>
94 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|