3d36faa49d
При повторной привязке Seaca переиспользуется запись device_uuid вместо INSERT.
Добавлены POST /devices/{id}/revoke и DELETE для полного удаления записи в настройках SAC.
401 lines
9.8 KiB
TypeScript
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[];
|
|
}
|