Bislang war die einzige Möglichkeit, die laufende Manticore-/PostgreSQL-/ Postfix-/nginx-Version zu sehen, SSH + <binary> --version. serviceVersion() löst das best-effort pro Dienst auf (archivmail/-web: appVersion-Konstante, manticore: searchd --version, postgresql: psql --version, postfix: postconf mail_version, nginx: nginx -v). Fehler werden verschluckt (leerer String), eine unbekannte Version darf den Dienst-Status nicht auf "Fehler" kippen. manticore war bisher gar nicht in der Dienste-Whitelist (allowedServices) — jetzt ergänzt, damit es überhaupt in der Liste auftaucht und start/stop/restart wie die anderen Dienste möglich ist.
267 lines
7.3 KiB
TypeScript
267 lines
7.3 KiB
TypeScript
import { API_BASE, request } from "./core";
|
|
|
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface HealthResponse {
|
|
status: string;
|
|
}
|
|
|
|
export interface SystemInfo {
|
|
fqdn: string;
|
|
imap_port: number;
|
|
imap_port_alt: number;
|
|
}
|
|
|
|
export interface SMTPStatus {
|
|
// global daemon fields (superadmin)
|
|
running?: boolean;
|
|
enabled?: boolean;
|
|
bind?: string;
|
|
domain?: string;
|
|
tls?: boolean;
|
|
max_size_mb?: number;
|
|
allowed_ips?: string[];
|
|
received?: number;
|
|
rejected?: number;
|
|
last_mail_at?: string;
|
|
// tenant-scoped fields (domain_admin)
|
|
tenant_only?: boolean;
|
|
domains?: string[];
|
|
total_mails?: number;
|
|
total_bytes?: number;
|
|
}
|
|
|
|
export interface StorageStats {
|
|
total_mails: number;
|
|
total_bytes: number;
|
|
}
|
|
|
|
export interface ServiceStatus {
|
|
name: string;
|
|
display_name: string;
|
|
active: string;
|
|
sub: string;
|
|
enabled: string;
|
|
description: string;
|
|
version?: string;
|
|
external_blocked?: boolean;
|
|
}
|
|
|
|
export interface AuditEntry {
|
|
id: string;
|
|
timestamp: string;
|
|
event_type: string;
|
|
username: string;
|
|
detail: string;
|
|
}
|
|
|
|
export interface AuditResponse {
|
|
total: number;
|
|
entries: AuditEntry[];
|
|
}
|
|
|
|
export interface SystemStatsCPU {
|
|
load1: number;
|
|
load5: number;
|
|
load15: number;
|
|
num_cpu: number;
|
|
}
|
|
|
|
export interface SystemStatsRAM {
|
|
total_bytes: number;
|
|
used_bytes: number;
|
|
free_bytes: number;
|
|
used_pct: number;
|
|
}
|
|
|
|
export interface SystemStatsDisk {
|
|
mount: string;
|
|
total_bytes: number;
|
|
used_bytes: number;
|
|
free_bytes: number;
|
|
used_pct: number;
|
|
fstype: string;
|
|
}
|
|
|
|
export interface SystemStatsMailInfo {
|
|
id: string;
|
|
date: string;
|
|
from: string;
|
|
subject: string;
|
|
}
|
|
|
|
export interface SystemStatsActivity {
|
|
last_60_min: number;
|
|
last_24h: number;
|
|
last_7d: number;
|
|
last_30d: number;
|
|
}
|
|
|
|
export interface SystemStatsEstimate {
|
|
avg_mails_per_day: number;
|
|
avg_mail_bytes: number;
|
|
days_until_full: number; // -1 = unknown
|
|
archive_age_days: number;
|
|
}
|
|
|
|
export interface SystemStats {
|
|
cpu: SystemStatsCPU;
|
|
ram: SystemStatsRAM;
|
|
disks: SystemStatsDisk[];
|
|
uptime: { seconds: number };
|
|
archive: {
|
|
first_mail: SystemStatsMailInfo | null;
|
|
last_mail: SystemStatsMailInfo | null;
|
|
};
|
|
activity: SystemStatsActivity;
|
|
estimate: SystemStatsEstimate;
|
|
}
|
|
|
|
export interface TimeseriesPoint {
|
|
day: string; // "2026-04-05"
|
|
count: number;
|
|
}
|
|
|
|
export interface SecurityCheck {
|
|
name: string;
|
|
status: "ok" | "warning" | "error";
|
|
message: string;
|
|
}
|
|
|
|
export interface SecurityAuditResult {
|
|
checks: SecurityCheck[];
|
|
run_at: string;
|
|
}
|
|
|
|
export interface CertInfo {
|
|
exists: boolean;
|
|
subject?: string;
|
|
issuer?: string;
|
|
not_before?: string;
|
|
not_after?: string;
|
|
dns_names?: string[];
|
|
ip_addresses?: string[];
|
|
fingerprint_sha256?: string;
|
|
is_self_signed?: boolean;
|
|
days_remaining?: number;
|
|
}
|
|
|
|
export interface SelfSignedRequest {
|
|
common_name: string;
|
|
dns_names: string[];
|
|
ip_addresses: string[];
|
|
validity_years: number;
|
|
}
|
|
|
|
export interface ACMERequest {
|
|
domain: string;
|
|
email: string;
|
|
}
|
|
|
|
// ── Health & Stats ────────────────────────────────────────────────────────────
|
|
|
|
export async function getHealth(): Promise<HealthResponse> {
|
|
return request<HealthResponse>("/api/health");
|
|
}
|
|
|
|
export async function getSystemInfo(): Promise<SystemInfo> {
|
|
return request<SystemInfo>("/api/system/info");
|
|
}
|
|
|
|
export async function getSMTPStatus(): Promise<SMTPStatus> {
|
|
return request<SMTPStatus>("/api/admin/smtp/status");
|
|
}
|
|
|
|
export async function getStorageStats(): Promise<StorageStats> {
|
|
return request<StorageStats>("/api/admin/storage/stats");
|
|
}
|
|
|
|
export async function getSystemStats(): Promise<SystemStats> {
|
|
return request<SystemStats>("/api/admin/system/stats");
|
|
}
|
|
|
|
export async function getMailTimeseries(days = 30): Promise<{ days: number; points: TimeseriesPoint[] }> {
|
|
return request<{ days: number; points: TimeseriesPoint[] }>(`/api/admin/stats/timeseries?days=${days}`);
|
|
}
|
|
|
|
// ── Services ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function getServices(): Promise<ServiceStatus[]> {
|
|
return request<ServiceStatus[]>("/api/admin/services");
|
|
}
|
|
|
|
export async function serviceAction(
|
|
name: string,
|
|
action: "start" | "stop" | "restart" | "enable" | "disable" | "block_external" | "allow_external"
|
|
): Promise<ServiceStatus> {
|
|
return request<ServiceStatus>(`/api/admin/services/${encodeURIComponent(name)}/action`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ action }),
|
|
});
|
|
}
|
|
|
|
// ── Audit ─────────────────────────────────────────────────────────────────────
|
|
|
|
export async function getAuditLog(params: {
|
|
page?: number;
|
|
page_size?: number;
|
|
username?: string;
|
|
event_type?: string;
|
|
}): Promise<AuditResponse> {
|
|
const sp = new URLSearchParams();
|
|
if (params.page) sp.set("page", String(params.page));
|
|
if (params.page_size) sp.set("page_size", String(params.page_size));
|
|
if (params.username) sp.set("username", params.username);
|
|
if (params.event_type) sp.set("event_type", params.event_type);
|
|
return request<AuditResponse>(`/api/audit?${sp.toString()}`);
|
|
}
|
|
|
|
// ── Security ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function getSecurityAudit(): Promise<SecurityAuditResult> {
|
|
return request<SecurityAuditResult>("/api/admin/security/audit");
|
|
}
|
|
|
|
export async function fixSecurityIssue(action: string): Promise<{ message: string }> {
|
|
return request<{ message: string }>("/api/admin/security/fix", {
|
|
method: "POST",
|
|
body: JSON.stringify({ action }),
|
|
});
|
|
}
|
|
|
|
// ── Certificates ──────────────────────────────────────────────────────────────
|
|
|
|
export async function getCertInfo(): Promise<CertInfo> {
|
|
return request<CertInfo>("/api/admin/cert/info");
|
|
}
|
|
|
|
export async function uploadCert(cert: File, key: File): Promise<{ ok: boolean; message: string }> {
|
|
const form = new FormData();
|
|
form.append("cert", cert);
|
|
form.append("key", key);
|
|
const res = await fetch(`${API_BASE}/api/admin/cert/upload`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
body: form,
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(body || `Upload failed: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export async function generateSelfSignedCert(req: SelfSignedRequest): Promise<CertInfo & { ok: boolean }> {
|
|
return request<CertInfo & { ok: boolean }>("/api/admin/cert/self-signed", {
|
|
method: "POST",
|
|
body: JSON.stringify(req),
|
|
});
|
|
}
|
|
|
|
export async function requestACMECert(req: ACMERequest): Promise<{ ok: boolean; output: string }> {
|
|
return request<{ ok: boolean; output: string }>("/api/admin/cert/acme", {
|
|
method: "POST",
|
|
body: JSON.stringify(req),
|
|
});
|
|
}
|