'use client'; import { useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { ChevronRight, Clock3, Fingerprint, KeyRound, Laptop, LogOut, Mail, Monitor, type LucideIcon, Phone, ShieldCheck, Smartphone, Speaker, Tv } from 'lucide-react'; import { useAuth } from '@/components/id/auth-provider'; import { IdShell } from '@/components/id/shell'; import { useToast } from '@/components/id/toast-provider'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { ActiveDevice, apiFetch, SignInEvent } from '@/lib/api'; import { useRequireAuth } from '@/hooks/use-require-auth'; function formatDate(value: string) { return new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }).format(new Date(value)); } const deviceIcons: Record = { WEB: Monitor, DESKTOP: Monitor, IOS: Smartphone, ANDROID: Smartphone, TV: Tv, SMART_SPEAKER: Speaker, OTHER: Laptop }; type DialogMode = 'pin' | 'password' | 'backupEmail' | 'backupPhone' | null; const dialogConfig: Record, { title: string; placeholder: string; type: string }> = { pin: { title: 'Установить PIN-код', placeholder: 'PIN из 4-6 цифр', type: 'password' }, password: { title: 'Установить пароль', placeholder: 'Новый пароль (мин. 8 символов)', type: 'password' }, backupEmail: { title: 'Резервная почта', placeholder: 'backup@example.com', type: 'email' }, backupPhone: { title: 'Резервный телефон', placeholder: '+79991234567', type: 'tel' } }; export default function SecurityPage() { const router = useRouter(); const { user, token, logout } = useAuth(); const { isReady } = useRequireAuth(); const { showToast } = useToast(); const [devices, setDevices] = useState([]); const [history, setHistory] = useState([]); const [isSecurityLoading, setIsSecurityLoading] = useState(true); const [isRevoking, setIsRevoking] = useState(false); const [dialogMode, setDialogMode] = useState(null); const [dialogValue, setDialogValue] = useState(''); const [isDialogSaving, setIsDialogSaving] = useState(false); const loadSecurity = useCallback(async () => { if (!user || !token) return; setIsSecurityLoading(true); try { const [devicesResponse, historyResponse] = await Promise.all([ apiFetch<{ devices: ActiveDevice[] }>(`/security/users/${user.id}/devices`, {}, token), apiFetch<{ events: SignInEvent[] }>(`/security/users/${user.id}/sign-in-history`, {}, token) ]); setDevices(devicesResponse.devices ?? []); setHistory(historyResponse.events ?? []); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить безопасность'); } finally { setIsSecurityLoading(false); } }, [showToast, token, user]); useEffect(() => { if (!isReady || !user) return; void loadSecurity(); }, [isReady, loadSecurity, user]); async function revokeDevice(deviceId: string) { if (!user || !token) return; try { await apiFetch(`/security/users/${user.id}/devices/${deviceId}/revoke`, { method: 'POST' }, token); setDevices((current) => current.filter((device) => device.id !== deviceId)); showToast('Сессии устройства завершены'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось завершить сессию'); } } async function revokeAllSessions() { if (!user || !token) return; setIsRevoking(true); try { await apiFetch(`/security/users/${user.id}/revoke-all-sessions`, { method: 'POST' }, token); showToast('Все сессии завершены'); logout(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось выйти везде'); } finally { setIsRevoking(false); } } async function submitDialog() { if (!user || !token || !dialogMode) return; setIsDialogSaving(true); try { if (dialogMode === 'pin') { await apiFetch(`/security/users/${user.id}/pin/setup`, { method: 'POST', body: JSON.stringify({ pin: dialogValue.trim() }) }, token); showToast('PIN-код установлен'); } else if (dialogMode === 'password') { await apiFetch(`/profile/users/${user.id}/password`, { method: 'POST', body: JSON.stringify({ password: dialogValue }) }, token); showToast('Пароль установлен'); } else if (dialogMode === 'backupEmail') { await apiFetch(`/profile/users/${user.id}/contacts`, { method: 'PATCH', body: JSON.stringify({ backupEmail: dialogValue.trim() }) }, token); showToast('Резервная почта добавлена'); } else if (dialogMode === 'backupPhone') { await apiFetch(`/profile/users/${user.id}/contacts`, { method: 'PATCH', body: JSON.stringify({ backupPhone: dialogValue.trim() }) }, token); showToast('Резервный телефон добавлен'); } setDialogMode(null); setDialogValue(''); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось сохранить'); } finally { setIsDialogSaving(false); } } function openDialog(mode: Exclude) { setDialogValue(''); setDialogMode(mode); } return (

Безопасность

Устройства

На них вы вошли в ID и получаете уведомления безопасности от сервисов

{isSecurityLoading ? (
Загружаем устройства...
) : devices.length ? ( devices.map((device) => { const Icon = deviceIcons[device.type] ?? Laptop; return (
{device.name} {device.lastIp ?? 'IP не сохранён'} · {formatDate(device.lastSeenAt)}
); }) ) : (
Активных устройств пока нет
)}

Способ входа в профиль

Способы восстановления

Активность

{isSecurityLoading ? (
Загружаем историю входов...
) : history.length ? ( history.map((item) => (
{item.success ? 'Успешный вход' : item.reason ?? 'Неудачная попытка входа'} {formatDate(item.createdAt)}
)) ) : (
История входов пока пуста
)}
(open ? null : setDialogMode(null))}> {dialogMode ? ( <> {dialogConfig[dialogMode].title}
setDialogValue(event.target.value)} />
) : null}
); }