Files
archivmail/src/lib/api.ts
T
sysops b95c49daa5 feat(PROJ-2): EML/MBOX-Upload für alle Benutzer zugänglich
- Backend: neue Routen POST /api/upload + GET /api/upload/{jobID}/progress (nur Auth, kein Admin)
- api.ts: uploadMailFilesUser + getUploadProgressUser für /api/upload
- search/page.tsx: Importieren-Button + Upload-Dialog mit Drag-and-Drop, Fortschrittsanzeige und automatischer Suchlisten-Aktualisierung nach Import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:12:09 +01:00

506 lines
13 KiB
TypeScript

import { clearAuthCache } from "@/lib/auth-cache";
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
async function request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(options.headers as Record<string, string>),
};
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers,
credentials: "include",
});
if (res.status === 401) {
clearAuthCache();
throw new Error("Unauthorized");
}
if (!res.ok) {
const body = await res.text();
throw new Error(body || `Request failed: ${res.status}`);
}
if (res.status === 204) return {} as T;
return res.json();
}
// Types
export interface LoginResponse {
user: {
id: number;
username: string;
email: string;
role: string;
};
}
export interface User {
id: number;
username: string;
email: string;
role: string;
active: boolean;
}
export interface MeResponse {
username: string;
role: string;
email: string;
}
export interface SMTPStatus {
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;
}
export interface HealthResponse {
status: string;
}
export interface SearchHit {
id: string;
score: number;
from?: string;
to?: string;
subject?: string;
date?: string;
size?: number;
has_attachments?: boolean;
}
export interface SearchResponse {
total: number;
hits: SearchHit[];
}
export interface MailAttachment {
index: number;
filename: string;
content_type: string;
size: number;
}
export interface MailDetail {
id: string;
from: string;
to: string;
cc?: string;
subject: string;
date: string;
size: number;
body_html?: string;
body_plain?: string;
raw_headers: string;
attachments: MailAttachment[];
verify_ok: boolean | null;
verified_at: string | null;
}
export interface AuditEntry {
id: string;
timestamp: string;
event_type: string;
username: string;
detail: string;
}
export interface AuditResponse {
total: number;
entries: AuditEntry[];
}
export interface CreateUserRequest {
username: string;
email: string;
password: string;
role: string;
}
export interface UpdateUserRequest {
email?: string;
role?: string;
active?: boolean;
password?: string;
}
// API functions
export async function login(
username: string,
password: string
): Promise<LoginResponse> {
return request<LoginResponse>("/api/auth/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
}
export async function getMe(): Promise<MeResponse> {
return request<MeResponse>("/api/auth/me");
}
export async function logout(): Promise<void> {
clearAuthCache();
await request<void>("/api/auth/logout", { method: "POST" });
}
export async function searchEmails(params: {
q?: string;
from?: string;
to?: string;
date_from?: string;
date_to?: string;
sort?: string;
has_attachment?: boolean;
page?: number;
page_size?: number;
}): Promise<SearchResponse> {
const sp = new URLSearchParams();
if (params.q) sp.set("q", params.q);
if (params.from) sp.set("from", params.from);
if (params.to) sp.set("to", params.to);
if (params.date_from) sp.set("date_from", params.date_from);
if (params.date_to) sp.set("date_to", params.date_to);
if (params.sort) sp.set("sort", params.sort);
if (params.has_attachment !== undefined) sp.set("has_attachment", String(params.has_attachment));
if (params.page) sp.set("page", String(params.page));
if (params.page_size) sp.set("page_size", String(params.page_size));
return request<SearchResponse>(`/api/search?${sp.toString()}`);
}
export async function getUsers(): Promise<User[]> {
return request<User[]>("/api/users");
}
export async function createUser(data: CreateUserRequest): Promise<User> {
return request<User>("/api/users", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateUser(id: number, data: UpdateUserRequest): Promise<User> {
return request<User>(`/api/users/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
}
export async function deleteUser(id: number): Promise<void> {
await request<void>(`/api/users/${id}`, { method: "DELETE" });
}
export interface StorageStats {
total_mails: number;
total_bytes: number;
}
export async function getStorageStats(): Promise<StorageStats> {
return request<StorageStats>("/api/admin/storage/stats");
}
export async function getSMTPStatus(): Promise<SMTPStatus> {
return request<SMTPStatus>("/api/admin/smtp/status");
}
export async function getHealth(): Promise<HealthResponse> {
return request<HealthResponse>("/api/health");
}
export async function getMail(id: string): Promise<MailDetail> {
return request<MailDetail>(`/api/mails/${id}`);
}
export async function downloadMailAttachment(
id: string,
index: number
): Promise<{ blob: Blob; filename: string }> {
const res = await fetch(`${API_BASE}/api/mails/${id}/attachments/${index}`, {
credentials: "include",
});
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
const disposition = res.headers.get("Content-Disposition") || "";
const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
const filename = match ? match[1].replace(/['"]/g, "") : `anhang-${index}`;
return { blob: await res.blob(), filename };
}
export async function downloadMailRaw(
id: string
): Promise<{ blob: Blob; filename: string }> {
const res = await fetch(`${API_BASE}/api/mails/${id}/raw`, {
credentials: "include",
});
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
return { blob: await res.blob(), filename: `${id}.eml` };
}
export interface ServiceStatus {
name: string;
display_name: string;
active: string;
sub: string;
enabled: string;
description: string;
external_blocked?: boolean;
}
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 }),
});
}
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()}`);
}
// ── IMAP ──────────────────────────────────────────────────────────────────
export interface ImapFolder {
name: string;
excluded: boolean;
reason?: string;
}
export interface ImapAccount {
id: number;
owner: string;
name: string;
host: string;
port: number;
tls: string;
username: string;
excluded_folders: string[];
status: string;
error_msg: string;
last_import_at?: string;
last_import_count: number;
progress_current: number;
progress_total: number;
created_at: string;
// PROJ-8: Auto-sync fields
sync_interval_min: number;
last_sync_at?: string;
last_sync_count: number;
sync_running: boolean;
sync_status: string;
sync_error_msg: string;
}
export interface ImapTestResult {
ok: boolean;
folders?: ImapFolder[];
error?: string;
}
export async function getImapAccounts(): Promise<ImapAccount[]> {
return request<ImapAccount[]>("/api/imap");
}
export async function createImapAccount(data: {
name: string;
host: string;
port: number;
tls: string;
username: string;
password: string;
excluded_folders: string[];
}): Promise<ImapAccount> {
return request<ImapAccount>("/api/imap", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function deleteImapAccount(id: number): Promise<void> {
await request<void>(`/api/imap/${id}`, { method: "DELETE" });
}
export async function testImapConnection(data: {
host: string;
port: number;
tls: string;
username: string;
password: string;
}): Promise<ImapTestResult> {
return request<ImapTestResult>("/api/imap/test", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function startImapImport(id: number): Promise<ImapAccount> {
return request<ImapAccount>(`/api/imap/${id}/import`, { method: "POST" });
}
export async function getImapProgress(id: number): Promise<ImapAccount> {
return request<ImapAccount>(`/api/imap/${id}/progress`);
}
export async function triggerImapSync(id: number): Promise<ImapAccount> {
return request<ImapAccount>(`/api/imap/${id}/sync`, { method: "POST" });
}
export async function updateImapInterval(id: number, intervalMin: number): Promise<ImapAccount> {
return request<ImapAccount>(`/api/imap/${id}`, {
method: "PATCH",
body: JSON.stringify({ sync_interval_min: intervalMin }),
});
}
// ── System Stats ──────────────────────────────────────────────────────────
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 SystemStats {
cpu: SystemStatsCPU;
ram: SystemStatsRAM;
disks: SystemStatsDisk[];
archive: {
first_mail: SystemStatsMailInfo | null;
last_mail: SystemStatsMailInfo | null;
};
}
export async function getSystemStats(): Promise<SystemStats> {
return request<SystemStats>("/api/admin/system/stats");
}
// ── Export ────────────────────────────────────────────────────────────────
export async function exportMailPDF(id: string): Promise<{ blob: Blob; filename: string }> {
const res = await fetch(`${API_BASE}/api/export/pdf/${id}`, {
credentials: "include",
});
if (!res.ok) throw new Error("PDF export failed");
const blob = await res.blob();
const cd = res.headers.get("Content-Disposition") || "";
const filename = cd.match(/filename="([^"]+)"/)?.[1] || `${id.slice(0, 16)}.pdf`;
return { blob, filename };
}
export async function exportMailsZIP(ids: string[], attachments: boolean): Promise<{ blob: Blob }> {
const res = await fetch(`${API_BASE}/api/export/zip`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ ids, attachments }),
});
if (!res.ok) throw new Error("ZIP export failed");
return { blob: await res.blob() };
}
// ── Upload ────────────────────────────────────────────────────────────────
export interface UploadJob {
id: string;
status: "running" | "done" | "error";
total: number;
imported: number;
skipped: number;
errors: number;
error_msg?: string;
}
export async function uploadMailFiles(files: File[]): Promise<{ job_id: string }> {
const form = new FormData();
for (const f of files) form.append("files", f);
const res = await fetch(`${API_BASE}/api/admin/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 getUploadProgress(jobID: string): Promise<UploadJob> {
return request<UploadJob>(`/api/admin/upload/${jobID}/progress`);
}
export async function uploadMailFilesUser(files: File[]): Promise<{ job_id: string }> {
const form = new FormData();
for (const f of files) form.append("files", f);
const res = await fetch(`${API_BASE}/api/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 getUploadProgressUser(jobID: string): Promise<UploadJob> {
return request<UploadJob>(`/api/upload/${jobID}/progress`);
}