Files
security-alert-center/frontend/src/api.ts
T
PapaTramp 3d36faa49d fix(mobile): повторный enroll и удаление устройств в UI (0.9.10).
При повторной привязке Seaca переиспользуется запись device_uuid вместо INSERT.
Добавлены POST /devices/{id}/revoke и DELETE для полного удаления записи в настройках SAC.
2026-06-13 16:36:07 +10:00

401 lines
9.8 KiB
TypeScript

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 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<T>(path: string, init: RequestInit = {}): Promise<T> {
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 });
const text = await res.text();
if (res.status === 401) {
if (hadToken) {
clearToken();
redirectToErrorPage(401);
}
throw new ApiError(parseApiError(text) || "Unauthorized", 401);
}
if (res.status === 403) {
if (hadToken) {
redirectToErrorPage(403);
}
throw new ApiError(parseApiError(text) || "Forbidden", 403);
}
if (res.status === 429) {
redirectToErrorPage(429);
throw new ApiError(parseApiError(text) || "Too many requests", 429);
}
if (!res.ok) {
throw new ApiError(parseApiError(text) || 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<MeResponse> {
return apiFetch<MeResponse>("/api/v1/auth/me");
}
export function parseApiError(text: string): string {
if (!text) return "";
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;
}
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";
}
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;
}
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>;
}
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 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[];
}