import { clearAuthCache } from "@/lib/auth-cache"; const API_BASE = process.env.NEXT_PUBLIC_API_URL || ""; async function request( path: string, options: RequestInit = {} ): Promise { const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; 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 { // 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 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 { return request("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); } export async function getMe(): Promise { return request("/api/auth/me"); } export async function logout(): Promise { clearAuthCache(); await request("/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 { 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(`/api/search?${sp.toString()}`); } export async function getUsers(): Promise { return request("/api/users"); } export async function createUser(data: CreateUserRequest): Promise { return request("/api/users", { method: "POST", body: JSON.stringify(data), }); } export async function updateUser(id: number, data: UpdateUserRequest): Promise { return request(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(data), }); } export async function deleteUser(id: number): Promise { await request(`/api/users/${id}`, { method: "DELETE" }); } export interface StorageStats { total_mails: number; total_bytes: number; } export async function getStorageStats(): Promise { return request("/api/admin/storage/stats"); } export async function getSMTPStatus(): Promise { return request("/api/admin/smtp/status"); } export async function getHealth(): Promise { return request("/api/health"); } export async function getMail(id: string): Promise { return request(`/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 { return request("/api/admin/services"); } export async function serviceAction( name: string, action: "start" | "stop" | "restart" | "enable" | "disable" | "block_external" | "allow_external" ): Promise { return request(`/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 { 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(`/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 { return request("/api/imap"); } export async function createImapAccount(data: { name: string; host: string; port: number; tls: string; username: string; password: string; excluded_folders: string[]; }): Promise { return request("/api/imap", { method: "POST", body: JSON.stringify(data), }); } export async function deleteImapAccount(id: number): Promise { await request(`/api/imap/${id}`, { method: "DELETE" }); } export async function testImapConnection(data: { host: string; port: number; tls: string; username: string; password: string; }): Promise { return request("/api/imap/test", { method: "POST", body: JSON.stringify(data), }); } export async function startImapImport(id: number): Promise { return request(`/api/imap/${id}/import`, { method: "POST" }); } export async function getImapProgress(id: number): Promise { return request(`/api/imap/${id}/progress`); } export async function triggerImapSync(id: number): Promise { return request(`/api/imap/${id}/sync`, { method: "POST" }); } export async function updateImapInterval(id: number, intervalMin: number): Promise { return request(`/api/imap/${id}`, { method: "PATCH", body: JSON.stringify({ sync_interval_min: intervalMin }), }); } // ── POP3 ────────────────────────────────────────────────────────────────── export interface Pop3Account { id: number; owner: string; name: string; host: string; port: number; tls: string; tls_skip_verify: boolean; username: string; status: string; error_msg: string; last_import_at?: string; last_import_count: number; progress_current: number; progress_total: number; created_at: string; } export interface Pop3TestResult { ok: boolean; message: string; message_count?: number; total_size_bytes?: number; } export async function getPop3Accounts(): Promise { return request("/api/pop3"); } export async function createPop3Account(data: { name: string; host: string; port: number; tls: string; tls_skip_verify: boolean; username: string; password: string; }): Promise { return request("/api/pop3", { method: "POST", body: JSON.stringify(data), }); } export async function deletePop3Account(id: number): Promise { await request(`/api/pop3/${id}`, { method: "DELETE" }); } export async function testPop3Connection(data: { host: string; port: number; tls: string; tls_skip_verify: boolean; username: string; password: string; }): Promise { return request("/api/pop3/test", { method: "POST", body: JSON.stringify(data), }); } export async function startPop3Import(id: number): Promise { return request(`/api/pop3/${id}/import`, { method: "POST" }); } export async function getPop3Progress(id: number): Promise { return request(`/api/pop3/${id}/progress`); } // ── 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 { return request("/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 { return request(`/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 { return request(`/api/upload/${jobID}/progress`); } // ── Security Audit ──────────────────────────────────────────────────────── export interface SecurityCheck { name: string; status: "ok" | "warning" | "error"; message: string; } export interface SecurityAuditResult { checks: SecurityCheck[]; run_at: string; } export async function getSecurityAudit(): Promise { return request("/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 }), }); } // ── LDAP ────────────────────────────────────────────────────────────────── export interface LDAPGroupMapping { group_dn: string; role: string; } export interface LDAPConfig { id?: number; enabled: boolean; url: string; bind_dn: string; bind_password: string; base_dn: string; user_filter: string; tls: boolean; tls_skip_verify: boolean; default_role: string; group_mappings: LDAPGroupMapping[]; updated_at?: string; updated_by?: string; } export interface LDAPTestResult { ok: boolean; message: string; latency_ms: number; server_info: string; users_found: number; error_detail: string; } export async function getLDAPConfig(): Promise { try { return await request("/api/admin/ldap"); } catch (e: unknown) { if (e instanceof Error && e.message.includes("404")) return null; if (e instanceof Error && e.message.includes("no ldap config")) return null; throw e; } } export async function saveLDAPConfig(cfg: Partial): Promise { await request("/api/admin/ldap", { method: "PUT", body: JSON.stringify(cfg), }); } export async function deleteLDAPConfig(): Promise { await request("/api/admin/ldap", { method: "DELETE" }); } export async function testLDAPConfig( payload: { use_saved: boolean } & Partial ): Promise { return request("/api/admin/ldap/test", { method: "POST", body: JSON.stringify(payload), }); } // ── Tenants ──────────────────────────────────────────────────────────────── export interface Tenant { id: number; name: string; slug: string; active: boolean; created_at: string; domain_count?: number; user_count?: number; ldap_enabled?: boolean; ldap_url?: string; } export interface TenantDomain { id: number; tenant_id: number; domain: string; created_at: string; } export async function getTenants(): Promise { return request("/api/tenants"); } export async function createTenant(name: string, slug: string): Promise { return request("/api/tenants", { method: "POST", body: JSON.stringify({ name, slug }), }); } export async function updateTenant( id: number, data: { name?: string; active?: boolean } ): Promise { return request(`/api/tenants/${id}`, { method: "PATCH", body: JSON.stringify(data), }); } export async function deleteTenant(id: number): Promise { await request(`/api/tenants/${id}`, { method: "DELETE" }); } export async function getTenantDomains(id: number): Promise { return request(`/api/tenants/${id}/domains`); } export async function addTenantDomain( tenantId: number, domain: string ): Promise { return request(`/api/tenants/${tenantId}/domains`, { method: "POST", body: JSON.stringify({ domain }), }); } export async function removeTenantDomain( tenantId: number, domainId: number ): Promise { await request(`/api/tenants/${tenantId}/domains/${domainId}`, { method: "DELETE", }); } // ── PROJ-23: Pro-Mandant LDAP (tenant_ldap) ────────────────────────────── export interface TenantLDAPConfig extends LDAPConfig { tenant_id?: number; } // domain_admin -- eigener Mandant export async function getTenantLDAPConfig(): Promise { try { return await request("/api/tenant/ldap"); } catch (e: unknown) { if (e instanceof Error && e.message.includes("404")) return null; if (e instanceof Error && e.message.includes("no ldap config")) return null; throw e; } } export async function saveTenantLDAPConfig(cfg: Partial): Promise { await request("/api/tenant/ldap", { method: "PUT", body: JSON.stringify(cfg) }); } export async function deleteTenantLDAPConfig(): Promise { await request("/api/tenant/ldap", { method: "DELETE" }); } export async function testTenantLDAPConfig( payload: { use_saved: boolean } & Partial ): Promise { return request("/api/tenant/ldap/test", { method: "POST", body: JSON.stringify(payload), }); } // superadmin -- beliebiger Mandant per ID export async function getAdminTenantLDAPConfig(tenantID: number): Promise { try { return await request(`/api/admin/tenants/${tenantID}/ldap`); } catch (e: unknown) { if (e instanceof Error && e.message.includes("404")) return null; if (e instanceof Error && e.message.includes("no ldap config")) return null; throw e; } } export async function saveAdminTenantLDAPConfig(tenantID: number, cfg: Partial): Promise { await request(`/api/admin/tenants/${tenantID}/ldap`, { method: "PUT", body: JSON.stringify(cfg) }); } export async function deleteAdminTenantLDAPConfig(tenantID: number): Promise { await request(`/api/admin/tenants/${tenantID}/ldap`, { method: "DELETE" }); } export async function testAdminTenantLDAPConfig( tenantID: number, payload: { use_saved: boolean } & Partial ): Promise { return request(`/api/admin/tenants/${tenantID}/ldap/test`, { method: "POST", body: JSON.stringify(payload), }); } // ── Profil-Einstellungen ────────────────────────────────────────────────── export async function changePassword( currentPassword: string, newPassword: string ): Promise<{ ok: boolean }> { return request<{ ok: boolean }>("/api/auth/password", { method: "PATCH", body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), }); } export async function changeEmail( email: string ): Promise<{ ok: boolean; email: string }> { return request<{ ok: boolean; email: string }>("/api/auth/email", { method: "PATCH", body: JSON.stringify({ email }), }); } // ── TOTP / 2FA ──────────────────────────────────────────────────────────── export async function getTOTPSetup(): Promise<{ secret: string; otpauth_url: string; qr_code: string }> { return request<{ secret: string; otpauth_url: string; qr_code_svg: string }>("/api/auth/totp/setup"); } export async function confirmTOTPSetup(code: string): Promise<{ ok: boolean }> { return request<{ ok: boolean }>("/api/auth/totp/setup", { method: "POST", body: JSON.stringify({ code }), }); } export async function disableTOTP(code: string): Promise<{ ok: boolean }> { return request<{ ok: boolean }>("/api/auth/totp", { method: "DELETE", body: JSON.stringify({ code }), }); }