const TOKEN_KEY = "sac_token"; const ROLE_KEY = "sac_role"; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function getRole(): string | null { return localStorage.getItem(ROLE_KEY); } export function isAdmin(): boolean { return getRole() === "admin"; } export function setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token); } export function setRole(role: string): void { localStorage.setItem(ROLE_KEY, role); } export function setAuth(token: string, role: string): void { setToken(token); setRole(role); } export function clearToken(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(ROLE_KEY); } export async function logoutApi(): Promise { try { await fetch("/api/v1/auth/logout", { method: "POST", credentials: "same-origin" }); } catch { /* ignore network errors on logout */ } } export class ApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "ApiError"; this.status = status; } } export function redirectToErrorPage(status: 401 | 403 | 429): void { window.location.href = `/${status}`; } export async function apiFetch(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); if (!headers.has("Content-Type") && init.body) { headers.set("Content-Type", "application/json"); } const token = getToken(); const hadToken = !!token; if (token) { headers.set("Authorization", `Bearer ${token}`); } const res = await fetch(path, { ...init, headers, credentials: "same-origin" }); const text = await res.text(); if (res.status === 401) { if (hadToken) { clearToken(); redirectToErrorPage(401); } throw new ApiError(parseApiError(text, 401) || "Unauthorized", 401); } if (res.status === 403) { if (hadToken) { redirectToErrorPage(403); } throw new ApiError(parseApiError(text, 403) || "Forbidden", 403); } if (res.status === 429) { redirectToErrorPage(429); throw new ApiError(parseApiError(text, 429) || "Too many requests", 429); } if (!res.ok) { throw new ApiError(parseApiError(text, res.status) || res.statusText, res.status); } if (!text) { return undefined as T; } return JSON.parse(text) as T; } export interface TokenResponse { access_token: string; token_type: string; username: string; role: string; } export interface MeResponse { username: string; role: string; } export function fetchMe(): Promise { return apiFetch("/api/v1/auth/me"); } export function parseApiError(text: string, status?: number): string { if (status === 502 || status === 503 || status === 504) { if (/\s*502 Bad Gateway/i.test(text) || /502 Bad Gateway/i.test(text)) { return "Сервер SAC временно недоступен (перезапуск или обновление). Подождите 5–10 секунд и обновите страницу."; } if (/<title>\s*503 Service Unavailable/i.test(text) || /503 Service Unavailable/i.test(text)) { return "SAC временно перегружен. Повторите запрос через несколько секунд."; } if (/<title>\s*504 Gateway Time-out/i.test(text) || /504 Gateway Time-out/i.test(text)) { return "Превышено время ожидания ответа SAC. Долгая операция могла ещё выполняться на сервере."; } return "Временная ошибка шлюза SAC. Обновите страницу."; } if (!text) return ""; if (text.trimStart().startsWith("<")) { return status ? `Ошибка HTTP ${status}` : "Ошибка сервера"; } try { const data = JSON.parse(text) as { detail?: string | Array<{ msg?: string }> }; if (typeof data.detail === "string") return data.detail; if (Array.isArray(data.detail)) { return data.detail.map((item) => item.msg ?? String(item)).join("; "); } } catch { /* plain text */ } return text; } export interface UserSummary { id: number; username: string; role: string; is_active: boolean; created_at: string; } export interface UserListResponse { items: UserSummary[]; } export interface UpdateUserPayload { username?: string; role?: string; password?: string; is_active?: boolean; } export function fetchUsers(): Promise<UserListResponse> { return apiFetch<UserListResponse>("/api/v1/users"); } export function createUser(payload: { username: string; password: string; role: string; }): Promise<UserSummary> { return apiFetch<UserSummary>("/api/v1/users", { method: "POST", body: JSON.stringify(payload), }); } export function updateUser(userId: number, payload: UpdateUserPayload): Promise<UserSummary> { return apiFetch<UserSummary>(`/api/v1/users/${userId}`, { method: "PATCH", body: JSON.stringify(payload), }); } export interface EventSummary { id: number; event_id: string; host_id: number; hostname: string; display_name?: string | null; product_version?: string | null; occurred_at: string; received_at: string; category: string; type: string; severity: string; title: string; summary: string; actor_user?: string | null; rdg_flap?: boolean; rdg_flap_pair_event_id?: number | null; rdg_flap_qwinsta_event_id?: number | null; rdg_access_path?: string | null; rdg_qwinsta_enabled?: boolean; session_terminated?: boolean; } export interface AgentCommandResponse { command_uuid: string; command_type: string; status: string; result_stdout?: string | null; result_stderr?: string | null; created_at?: string | null; completed_at?: string | null; target?: string | null; client_hostname?: string | null; internal_ip?: string | null; } export function postEventQwinsta(eventId: number): Promise<AgentCommandResponse> { return apiFetch<AgentCommandResponse>(`/api/v1/events/${eventId}/actions/qwinsta`, { method: "POST", }); } export function postEventLogoff(eventId: number, sessionId: number): Promise<AgentCommandResponse> { return apiFetch<AgentCommandResponse>(`/api/v1/events/${eventId}/actions/logoff`, { method: "POST", body: JSON.stringify({ session_id: sessionId }), }); } export function fetchEventAction(eventId: number, commandUuid: string): Promise<AgentCommandResponse> { return apiFetch<AgentCommandResponse>(`/api/v1/events/${eventId}/actions/${commandUuid}`); } export interface EventListResponse { items: EventSummary[]; total: number; page: number; page_size: number; } export interface EventDetail extends EventSummary { details: Record<string, unknown> | null; raw: Record<string, unknown> | null; payload: Record<string, unknown>; } export interface HostSummary { id: number; hostname: string; display_name: string | null; os_family: string; os_version?: string | null; product: string; product_version: string | null; ipv4: string | null; last_seen_at: string; event_count: number | null; last_heartbeat_at: string | null; last_daily_report_at: string | null; last_inventory_at?: string | null; agent_status: "online" | "stale" | "unknown"; version_status?: "ok" | "outdated" | "unknown"; pending_agent_update?: boolean; ssh_admin_ok?: boolean | null; ssh_admin_checked_at?: string | null; } export interface HostDetail extends HostSummary { agent_instance_id: string | null; ipv6: string | null; tags: string[] | null; use_sac_mode: string | null; inventory: Record<string, unknown> | null; inventory_updated_at: string | null; created_at: string | null; pending_update_requested_at?: string | null; pending_update_target_version?: string | null; agent_config_revision?: number; agent_config?: Record<string, unknown> | null; agent_update_state?: string | null; agent_update_last_at?: string | null; agent_update_last_error?: string | null; } export function fetchHost(id: number): Promise<HostDetail> { return apiFetch<HostDetail>(`/api/v1/hosts/${id}`); } export function fetchRecentEvents(limit = 1): Promise<EventSummary[]> { return apiFetch<EventSummary[]>(`/api/v1/dashboards/recent-events?limit=${limit}`); } export interface HostListResponse { items: HostSummary[]; total: number; page: number; page_size: number; latest_agent_versions?: Record<string, string>; /** Эталон для «устарела»: max(git latest, min, max в fleet) */ reference_agent_versions?: Record<string, string>; /** Последние версии из git (version.txt на main) */ git_latest_rdp_version?: string | null; git_latest_ssh_version?: string | null; } export interface ProblemSummary { id: number; host_id: number | null; hostname: string | null; display_name?: string | null; title: string; summary: string; severity: string; status: string; rule_id: string | null; fingerprint?: string; event_count?: number; last_seen_at?: string; created_at: string; updated_at: string; } export interface ProblemListResponse { items: ProblemSummary[]; total: number; page: number; page_size: number; } export interface ProblemEventItem { id: number; event_id: string; occurred_at: string; type: string; severity: string; title: string; summary: string; } export interface ProblemDetail extends ProblemSummary { events: ProblemEventItem[]; } export function fetchProblem(id: number): Promise<ProblemDetail> { return apiFetch<ProblemDetail>(`/api/v1/problems/${id}`); } export async function ackProblem(problemId: number): Promise<{ id: number; status: string }> { return apiFetch(`/api/v1/problems/${problemId}/ack`, { method: "POST" }); } export async function resolveProblem(problemId: number): Promise<{ id: number; status: string }> { return apiFetch(`/api/v1/problems/${problemId}/resolve`, { method: "POST" }); } export interface TopHostItem { host_id: number; hostname: string; display_name?: string | null; count: number; } export interface TopTypeItem { type: string; count: number; } export interface MobileSettings { devices_allowed: boolean; max_devices_per_user: number; min_app_version: string | null; fcm_enabled: boolean; fcm_configured: boolean; source: string; } export interface MobileEnrollmentCode { id: number; label: string; code_prefix: string; target_user_id: number | null; target_username: string | null; login_mode: string; max_uses: number; use_count: number; expires_at: string; revoked_at: string | null; created_at: string; is_active: boolean; } export interface MobileEnrollmentCodeCreated extends MobileEnrollmentCode { enrollment_code: string; } export interface MobileDevice { id: number; user_id: number; username: string; device_uuid: string; display_name: string; platform: string; app_version: string | null; has_fcm_token: boolean; enrolled_at: string; last_seen_at: string | null; revoked_at: string | null; is_active: boolean; } export function fetchMobileSettings(): Promise<MobileSettings> { return apiFetch<MobileSettings>("/api/v1/mobile/settings"); } export function updateMobileSettings(payload: { devices_allowed: boolean; max_devices_per_user: number; min_app_version?: string | null; }): Promise<MobileSettings> { return apiFetch<MobileSettings>("/api/v1/mobile/settings", { method: "PUT", body: JSON.stringify(payload), }); } export function fetchMobileEnrollmentCodes(): Promise<MobileEnrollmentCode[]> { return apiFetch<MobileEnrollmentCode[]>("/api/v1/mobile/enrollment-codes"); } export function createMobileEnrollmentCode(payload: { label?: string; target_user_id?: number | null; login_mode?: string; max_uses?: number; expires_in_hours?: number; }): Promise<MobileEnrollmentCodeCreated> { return apiFetch<MobileEnrollmentCodeCreated>("/api/v1/mobile/enrollment-codes", { method: "POST", body: JSON.stringify(payload), }); } export function revokeMobileEnrollmentCode(codeId: number): Promise<void> { return apiFetch<void>(`/api/v1/mobile/enrollment-codes/${codeId}`, { method: "DELETE" }); } export function fetchMobileDevices(): Promise<MobileDevice[]> { return apiFetch<MobileDevice[]>("/api/v1/mobile/devices"); } export function revokeMobileDevice(deviceId: number): Promise<MobileDevice> { return apiFetch<MobileDevice>(`/api/v1/mobile/devices/${deviceId}/revoke`, { method: "POST" }); } export function deleteMobileDevice(deviceId: number): Promise<void> { return apiFetch<void>(`/api/v1/mobile/devices/${deviceId}`, { method: "DELETE" }); } export function testMobileDevicePush(deviceId: number): Promise<{ message: string }> { return apiFetch(`/api/v1/mobile/devices/${deviceId}/test-push`, { method: "POST" }); } export interface WinAdminSettings { configured: boolean; user: string | null; password_hint: string | null; source: string; } export function fetchWinAdminSettings(): Promise<WinAdminSettings> { return apiFetch<WinAdminSettings>("/api/v1/settings/win-admin"); } export function updateWinAdminSettings(payload: { user?: string | null; password?: string | null; }): Promise<WinAdminSettings> { return apiFetch<WinAdminSettings>("/api/v1/settings/win-admin", { method: "PUT", body: JSON.stringify(payload), }); } export interface RdpFlapSettings { auto_disconnect: boolean; win_admin_configured: boolean; source: string; } export function fetchRdpFlapSettings(): Promise<RdpFlapSettings> { return apiFetch<RdpFlapSettings>("/api/v1/settings/rdp-flap"); } export function updateRdpFlapSettings(payload: { auto_disconnect: boolean }): Promise<RdpFlapSettings> { return apiFetch<RdpFlapSettings>("/api/v1/settings/rdp-flap", { method: "PUT", body: JSON.stringify(payload), }); } export interface HostWinRmTestResult { ok: boolean; message: string; target: string; hostname: string | null; } export function testHostWinRm(hostId: number): Promise<HostWinRmTestResult> { return apiFetch<HostWinRmTestResult>(`/api/v1/hosts/${hostId}/actions/winrm-test`, { method: "POST", }); } export interface LinuxAdminSettings { configured: boolean; user: string | null; password_hint: string | null; source: string; } export function fetchLinuxAdminSettings(): Promise<LinuxAdminSettings> { return apiFetch<LinuxAdminSettings>("/api/v1/settings/linux-admin"); } export function updateLinuxAdminSettings(payload: { user?: string | null; password?: string | null; }): Promise<LinuxAdminSettings> { return apiFetch<LinuxAdminSettings>("/api/v1/settings/linux-admin", { method: "PUT", body: JSON.stringify(payload), }); } export interface HostSshActionResult { ok: boolean; message: string; target: string; stdout: string | null; stderr: string | null; exit_code: number | null; product_version?: string | null; ssh_admin_ok?: boolean | null; } export function testHostSsh(hostId: number): Promise<HostSshActionResult> { return apiFetch<HostSshActionResult>(`/api/v1/hosts/${hostId}/actions/ssh-test`, { method: "POST", }); } export interface HostRemoteActionStartResult { status: string; host_id: number; title: string; message: string; } export interface HostRemoteActionJobStatus { active: boolean; host_id: number; hostname?: string | null; status?: string | null; title?: string; message?: string; stdout?: string | null; stderr?: string | null; output?: string | null; ok?: boolean | null; exit_code?: number | null; product_version?: string | null; agent_update_state?: string | null; started_at?: string | null; finished_at?: string | null; } export function updateHostAgentViaSsh(hostId: number): Promise<HostRemoteActionStartResult> { return apiFetch<HostRemoteActionStartResult>(`/api/v1/hosts/${hostId}/actions/agent-update`, { method: "POST", }); } export interface HostAgentUpdateRequestResult { status: string; pending_agent_update: boolean; target_version: string | null; agent_update_state: string | null; } export function requestHostAgentUpdate(hostId: number): Promise<HostAgentUpdateRequestResult> { return apiFetch<HostAgentUpdateRequestResult>( `/api/v1/hosts/${hostId}/actions/request-agent-update`, { method: "POST" }, ); } export function runHostAgentUpdateFallback(hostId: number): Promise<HostRemoteActionStartResult> { return apiFetch<HostRemoteActionStartResult>( `/api/v1/hosts/${hostId}/actions/agent-update-fallback`, { method: "POST" }, ); } export interface HostManualAddRequest { platform: "windows" | "linux"; target: string; } export function addHostManually(body: HostManualAddRequest): Promise<HostRemoteActionStartResult> { return apiFetch<HostRemoteActionStartResult>("/api/v1/hosts/manual-add", { method: "POST", body: JSON.stringify(body), }); } export function fetchHostRemoteJob(hostId: number): Promise<HostRemoteActionJobStatus> { return apiFetch<HostRemoteActionJobStatus>(`/api/v1/hosts/${hostId}/actions/remote-job`); } export interface HostSessionItem { session_id: string; user: string; tty?: string | null; state?: string | null; source_ip?: string | null; session_name?: string | null; } export interface HostSessionsResponse { ok: boolean; message: string; target?: string | null; sessions: HostSessionItem[]; } export interface HostSessionTerminateResponse { ok: boolean; message: string; target?: string | null; stdout?: string | null; stderr?: string | null; } export function fetchHostSessions(hostId: number): Promise<HostSessionsResponse> { return apiFetch<HostSessionsResponse>(`/api/v1/hosts/${hostId}/actions/sessions/list`, { method: "POST", }); } export function terminateHostSession( hostId: number, sessionId: string, ): Promise<HostSessionTerminateResponse> { return apiFetch<HostSessionTerminateResponse>(`/api/v1/hosts/${hostId}/actions/sessions/terminate`, { method: "POST", body: JSON.stringify({ session_id: sessionId }), }); } export function postEventTerminateSession( eventId: number, sessionId?: string, ): Promise<HostSessionTerminateResponse> { return apiFetch<HostSessionTerminateResponse>(`/api/v1/events/${eventId}/actions/terminate-session`, { method: "POST", body: JSON.stringify(sessionId ? { session_id: sessionId } : {}), }); } export interface HostAgentConfigResponse { config_revision: number; settings: Record<string, unknown>; } export function patchHostAgentConfig( hostId: number, settings: Record<string, unknown>, ): Promise<HostAgentConfigResponse> { return apiFetch<HostAgentConfigResponse>(`/api/v1/hosts/${hostId}/agent-config`, { method: "PATCH", body: JSON.stringify({ settings }), }); } export interface AgentUpdateSettings { mode: "gpo" | "sac"; fallback_enabled: boolean; fallback_after_minutes: number; recommended_rdp_version: string | null; recommended_ssh_version: string | null; min_rdp_version: string | null; min_ssh_version: string | null; win_agent_update_script: string | null; rdp_git_repo_url: string | null; ssh_git_repo_url: string | null; git_branch: string; git_latest_rdp_version: string | null; git_latest_ssh_version: string | null; git_fetched_at: string | null; git_errors: Record<string, string>; source: string; } export interface AgentGitTestResult { git_latest_rdp_version: string | null; git_latest_ssh_version: string | null; git_fetched_at: string | null; git_errors: Record<string, string>; from_cache: boolean; } export function fetchAgentUpdateSettings(): Promise<AgentUpdateSettings> { return apiFetch<AgentUpdateSettings>("/api/v1/settings/agent-updates"); } export function saveAgentUpdateSettings( payload: Partial<AgentUpdateSettings>, ): Promise<AgentUpdateSettings> { return apiFetch<AgentUpdateSettings>("/api/v1/settings/agent-updates", { method: "PUT", body: JSON.stringify(payload), }); } export interface AgentGitTestPayload { rdp_git_repo_url?: string | null; ssh_git_repo_url?: string | null; git_branch?: string | null; } export function testAgentGitRelease(payload?: AgentGitTestPayload): Promise<AgentGitTestResult> { return apiFetch<AgentGitTestResult>("/api/v1/settings/agent-updates/test-git", { method: "POST", body: JSON.stringify(payload ?? {}), }); } export interface LoginSecuritySettings { ip_whitelist: string[]; ip_whitelist_text: string; sync_fail2ban: boolean; max_failures: number; window_minutes: number; fail2ban_available: boolean; source: string; } export interface LoginBlockItem { ip_address: string; scope: string; failure_count: number | null; usernames: string[]; blocked_until: string | null; reason: string; } export function fetchLoginSecuritySettings(): Promise<LoginSecuritySettings> { return apiFetch<LoginSecuritySettings>("/api/v1/settings/login-security"); } export function saveLoginSecuritySettings(payload: { ip_whitelist_text: string; sync_fail2ban: boolean; }): Promise<LoginSecuritySettings> { return apiFetch<LoginSecuritySettings>("/api/v1/settings/login-security", { method: "PUT", body: JSON.stringify(payload), }); } export function fetchLoginSecurityBlocks(): Promise<LoginBlockItem[]> { return apiFetch<LoginBlockItem[]>("/api/v1/settings/login-security/blocks"); } export function unblockLoginSecurityIp(payload: { ip_address: string; scope: "web" | "ssh" | "both"; }): Promise<{ messages: string[] }> { return apiFetch<{ messages: string[] }>("/api/v1/settings/login-security/unblock", { method: "POST", body: JSON.stringify(payload), }); } export interface DashboardSummary { events_last_24h: number; hosts_total: number; hosts_stale: number; heartbeats_24h: number; daily_reports_24h: number; problems_open: number; problems_opened_24h: number; problems_resolved_24h: number; severity_24h: Record<string, number>; top_hosts: TopHostItem[]; top_event_types: TopTypeItem[]; recent_events: EventSummary[]; }