'use client'; import { useCallback, useEffect, useState } from 'react'; 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 { OtpInput } from '@/components/ui/otp-input'; import { ActiveDevice, apiFetch, SignInEvent, TotpSetupResponse, TotpStatusResponse } from '@/lib/api'; import { QRCodeSVG } from 'qrcode.react'; 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' | 'totpSetup' | 'totpDisable' | 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 { user, token } = 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 [totpEnabled, setTotpEnabled] = useState(false); const [totpSetup, setTotpSetup] = useState(null); const [totpCode, setTotpCode] = useState(''); const loadSecurity = useCallback(async () => { if (!user || !token) return; setIsSecurityLoading(true); try { const [devicesResponse, historyResponse, totpResponse] = await Promise.all([ apiFetch<{ devices: ActiveDevice[] }>(`/security/users/${user.id}/devices`, {}, token), apiFetch<{ events: SignInEvent[] }>(`/security/users/${user.id}/sign-in-history`, {}, token), apiFetch(`/security/users/${user.id}/totp/status`, {}, token) ]); setDevices(devicesResponse.devices ?? []); setHistory(historyResponse.events ?? []); setTotpEnabled(Boolean(totpResponse.isEnabled)); } 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('Выход выполнен на всех остальных устройствах'); await loadSecurity(); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось выйти везде'); } finally { setIsRevoking(false); } } async function openTotpDialog() { if (!user || !token) return; if (totpEnabled) { setTotpCode(''); setDialogMode('totpDisable'); return; } if (totpSetup) { setTotpCode(''); setDialogMode('totpSetup'); return; } setIsDialogSaving(true); try { const setup = await apiFetch(`/security/users/${user.id}/totp/setup`, { method: 'POST' }, token); setTotpSetup(setup); setTotpCode(''); setDialogMode('totpSetup'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось начать настройку аутентификатора'); } finally { setIsDialogSaving(false); } } async function submitTotpSetup(code: string) { if (!user || !token) return; setIsDialogSaving(true); try { await apiFetch(`/security/users/${user.id}/totp/enable`, { method: 'POST', body: JSON.stringify({ code }) }, token); setTotpEnabled(true); setDialogMode(null); setTotpSetup(null); setTotpCode(''); showToast('Приложение-аутентификатор подключено'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось подтвердить код'); } finally { setIsDialogSaving(false); } } async function submitTotpDisable(code: string) { if (!user || !token) return; setIsDialogSaving(true); try { await apiFetch(`/security/users/${user.id}/totp/disable`, { method: 'POST', body: JSON.stringify({ code }) }, token); setTotpEnabled(false); setDialogMode(null); setTotpCode(''); showToast('Приложение-аутентификатор отключено'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось отключить аутентификатор'); } finally { setIsDialogSaving(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)}
)) ) : (
История входов пока пуста
)}
{ if (!open) { setDialogMode(null); if (dialogMode !== 'totpSetup' || totpEnabled) { setTotpSetup(null); } setTotpCode(''); } }}> {dialogMode === 'totpSetup' && totpSetup ? ( <> Подключить аутентификатор

Отсканируйте QR-код в Google Authenticator или аналоге, затем введите 6-значный код.

Ключ: {totpSetup.secret}

void submitTotpSetup(code)} disabled={isDialogSaving} /> {isDialogSaving ?

Проверяем код...

: null} ) : null} {dialogMode === 'totpDisable' ? ( <> Отключить аутентификатор

Введите код из приложения, чтобы отключить двухфакторную аутентификацию.

void submitTotpDisable(code)} disabled={isDialogSaving} /> {isDialogSaving ?

Проверяем код...

: null} ) : null} {dialogMode && dialogMode !== 'totpSetup' && dialogMode !== 'totpDisable' ? ( <> {dialogConfig[dialogMode].title}
setDialogValue(event.target.value)} />
) : null}
); }