import axios from 'axios'; import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types'; // ============================================================================ // In-Memory-Token-Store // ============================================================================ // Der Access-Token wird BEWUSST nicht in localStorage gespeichert (XSS-Schutz). // Stattdessen lebt er im Modul-State + wird über den /api/auth/refresh-Endpoint // nach Page-Reload neu geholt (Refresh-Token sitzt in einem httpOnly-Cookie, // das JavaScript nie sieht). let accessToken: string | null = null; const tokenListeners = new Set<(t: string | null) => void>(); export function setAccessToken(t: string | null): void { accessToken = t; tokenListeners.forEach((l) => l(t)); } export function getAccessToken(): string | null { return accessToken; } export function subscribeToken(listener: (t: string | null) => void): () => void { tokenListeners.add(listener); return () => tokenListeners.delete(listener); } // ============================================================================ // Axios-Instance // ============================================================================ const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json' }, // withCredentials: Cookies werden bei same-origin-Requests mitgeschickt. // Wichtig für den /auth/refresh-Endpoint (liest den refresh_token-Cookie). withCredentials: true, }); // Request: Bearer-Header aus dem in-memory-Store api.interceptors.request.use((config) => { if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }); // Refresh-Retry-Mechanismus für 401-Antworten. // // Wenn der Access-Token abgelaufen ist (15-min-Lifetime), antwortet jeder // API-Aufruf mit 401. Der Interceptor probiert dann einmal /auth/refresh → // holt neuen Access-Token (Refresh-Token kommt automatisch via httpOnly-Cookie) // → wiederholt den ursprünglichen Request transparent. Wenn der Refresh selbst // scheitert (echt abgemeldet / Cookie weg): wir leiten zur Login-Seite um. // // Concurrent-Request-Protection: wenn 401 mehrfach parallel kommt, gibt's // nur einen aktiven refresh-Aufruf; alle wartenden Requests teilen sich das // Ergebnis. let refreshInflight: Promise | null = null; async function doRefresh(): Promise { if (refreshInflight) return refreshInflight; refreshInflight = (async () => { try { const res = await axios.post>( '/api/auth/refresh', {}, { withCredentials: true }, ); const newToken = res.data?.data?.token || null; setAccessToken(newToken); return newToken; } catch { setAccessToken(null); return null; } finally { refreshInflight = null; } })(); return refreshInflight; } api.interceptors.response.use( (response) => response, async (error) => { const original = error.config; const status = error.response?.status; const url: string = original?.url || ''; // Auth-Endpoints selbst nicht refreshen – sonst Endlos-Schleife const isAuthEndpoint = url.includes('/auth/login') || url.includes('/auth/customer-login') || url.includes('/auth/refresh') || url.includes('/auth/logout'); if (status === 401 && !isAuthEndpoint && !original?._retried) { original._retried = true; const newToken = await doRefresh(); if (newToken) { original.headers = original.headers || {}; original.headers.Authorization = `Bearer ${newToken}`; return api(original); } // Refresh fehlgeschlagen → echt abmelden + zur Login-Seite if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) { window.location.href = '/login'; } } const message = error.response?.data?.error || error.message || 'Ein Fehler ist aufgetreten'; return Promise.reject(new Error(message)); }, ); // Auth export const authApi = { login: async (email: string, password: string) => { const res = await api.post>('/auth/login', { email, password }); return res.data; }, customerLogin: async (email: string, password: string) => { const res = await api.post>('/auth/customer-login', { email, password }); return res.data; }, me: async () => { const res = await api.get>('/auth/me'); return res.data; }, logout: async () => { const res = await api.post>('/auth/logout'); return res.data; }, changeInitialPortalPassword: async (newPassword: string) => { const res = await api.post>('/auth/change-initial-portal-password', { newPassword }); return res.data; }, }; // Customers export const customerApi = { getAll: async (params?: { search?: string; type?: string; page?: number; limit?: number }) => { const res = await api.get>('/customers', { params }); return res.data; }, getById: async (id: number) => { const res = await api.get>(`/customers/${id}`); return res.data; }, create: async (data: Partial) => { const res = await api.post>('/customers', data); return res.data; }, update: async (id: number, data: Partial) => { const res = await api.put>(`/customers/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/customers/${id}`); return res.data; }, // Portal-Einstellungen getPortalSettings: async (customerId: number) => { const res = await api.get>(`/customers/${customerId}/portal`); return res.data; }, updatePortalSettings: async (customerId: number, data: { portalEnabled?: boolean; portalEmail?: string | null }) => { const res = await api.put>(`/customers/${customerId}/portal`, data); return res.data; }, setPortalPassword: async (customerId: number, password: string) => { const res = await api.post>(`/customers/${customerId}/portal/password`, { password }); return res.data; }, getPortalPassword: async (customerId: number) => { const res = await api.get>(`/customers/${customerId}/portal/password`); return res.data; }, generatePortalPassword: async (customerId: number) => { const res = await api.post>(`/customers/${customerId}/portal/password/generate`); return res.data; }, sendPortalCredentials: async (customerId: number) => { const res = await api.post>(`/customers/${customerId}/portal/send-credentials`); return res.data; }, // Vertreter-Verwaltung getRepresentatives: async (customerId: number) => { const res = await api.get>(`/customers/${customerId}/representatives`); return res.data; }, addRepresentative: async (customerId: number, representativeId: number, notes?: string) => { const res = await api.post>(`/customers/${customerId}/representatives`, { representativeId, notes }); return res.data; }, removeRepresentative: async (customerId: number, representativeId: number) => { const res = await api.delete>(`/customers/${customerId}/representatives/${representativeId}`); return res.data; }, searchForRepresentative: async (customerId: number, search: string) => { const res = await api.get>(`/customers/${customerId}/representatives/search`, { params: { search } }); return res.data; }, }; // Addresses export const addressApi = { getByCustomer: async (customerId: number) => { const res = await api.get>(`/customers/${customerId}/addresses`); return res.data; }, create: async (customerId: number, data: Partial
) => { const res = await api.post>(`/customers/${customerId}/addresses`, data); return res.data; }, update: async (id: number, data: Partial
) => { const res = await api.put>(`/addresses/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/addresses/${id}`); return res.data; }, }; // Bank Cards export const bankCardApi = { getByCustomer: async (customerId: number, showInactive = false) => { const res = await api.get>(`/customers/${customerId}/bank-cards`, { params: { showInactive } }); return res.data; }, create: async (customerId: number, data: Partial) => { const res = await api.post>(`/customers/${customerId}/bank-cards`, data); return res.data; }, update: async (id: number, data: Partial) => { const res = await api.put>(`/bank-cards/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/bank-cards/${id}`); return res.data; }, }; // Identity Documents export const documentApi = { getByCustomer: async (customerId: number, showInactive = false) => { const res = await api.get>(`/customers/${customerId}/documents`, { params: { showInactive } }); return res.data; }, create: async (customerId: number, data: Partial) => { const res = await api.post>(`/customers/${customerId}/documents`, data); return res.data; }, update: async (id: number, data: Partial) => { const res = await api.put>(`/documents/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/documents/${id}`); return res.data; }, }; // Meters export const meterApi = { getByCustomer: async (customerId: number, showInactive = false) => { const res = await api.get>(`/customers/${customerId}/meters`, { params: { showInactive } }); return res.data; }, create: async (customerId: number, data: Partial) => { const res = await api.post>(`/customers/${customerId}/meters`, data); return res.data; }, update: async (id: number, data: Partial) => { const res = await api.put>(`/meters/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/meters/${id}`); return res.data; }, getReadings: async (meterId: number) => { const res = await api.get>(`/meters/${meterId}/readings`); return res.data; }, addReading: async (meterId: number, data: Partial) => { const res = await api.post>(`/meters/${meterId}/readings`, data); return res.data; }, updateReading: async (meterId: number, readingId: number, data: Partial) => { const res = await api.put>(`/meters/${meterId}/readings/${readingId}`, data); return res.data; }, deleteReading: async (meterId: number, readingId: number) => { const res = await api.delete>(`/meters/${meterId}/readings/${readingId}`); return res.data; }, // Portal: Zählerstand melden reportReading: async (meterId: number, data: { value: number; readingDate?: string; notes?: string }) => { const res = await api.post>(`/meters/${meterId}/readings/report`, data); return res.data; }, getMyMeters: async () => { const res = await api.get>('/meters/my-meters'); return res.data; }, // Status-Update markTransferred: async (meterId: number, readingId: number) => { const res = await api.patch>(`/meters/${meterId}/readings/${readingId}/transfer`); return res.data; }, }; // Invoice API export const invoiceApi = { getInvoices: async (ecdId: number) => { const res = await api.get>(`/energy-details/${ecdId}/invoices`); return res.data; }, addInvoice: async (ecdId: number, data: Partial) => { const res = await api.post>(`/energy-details/${ecdId}/invoices`, data); return res.data; }, addInvoiceByContract: async (contractId: number, data: Partial) => { const res = await api.post>(`/contracts/${contractId}/invoices`, data); return res.data; }, updateInvoice: async (ecdId: number, invoiceId: number, data: Partial) => { const res = await api.put>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data); return res.data; }, deleteInvoice: async (ecdId: number, invoiceId: number) => { const res = await api.delete>(`/energy-details/${ecdId}/invoices/${invoiceId}`); return res.data; }, uploadDocument: async (invoiceId: number, file: File) => { const formData = new FormData(); formData.append('document', file); const res = await api.post>( `/upload/invoices/${invoiceId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } } ); return res.data; }, deleteDocument: async (invoiceId: number) => { const res = await api.delete>(`/upload/invoices/${invoiceId}`); return res.data; }, }; // Stressfrei-Wechseln E-Mail-Adressen export interface StressfreiEmail { id: number; customerId: number; email: string; platform?: string; notes?: string; isActive: boolean; isProvisioned?: boolean; hasMailbox: boolean; createdAt: string; updatedAt: string; } // Mailbox-Konto (für Dropdown-Auswahl) export interface MailboxAccount { id: number; email: string; notes?: string; hasMailbox: boolean; _count: { cachedEmails: number; }; } // Gecachte E-Mail export interface CachedEmail { id: number; stressfreiEmailId: number; folder: 'INBOX' | 'SENT'; messageId: string; uid: number; subject?: string; fromAddress: string; fromName?: string; toAddresses: string; // JSON Array ccAddresses?: string; // JSON Array receivedAt: string; textBody?: string; htmlBody?: string; hasAttachments: boolean; attachmentNames?: string; // JSON Array contractId?: number; assignedAt?: string; assignedBy?: number; isAutoAssigned?: boolean; // true = automatisch beim Senden aus Vertrag zugeordnet isRead: boolean; isStarred: boolean; isDeleted: boolean; deletedAt?: string; createdAt: string; updatedAt: string; stressfreiEmail?: { id: number; email: string; customerId: number; }; contract?: { id: number; contractNumber: string; } | null; } export interface SyncResult { newEmails: number; totalEmails: number; } export interface EmailAttachment { filename: string; content: string; // Base64-kodierter Inhalt contentType?: string; // MIME-Type (z.B. 'application/pdf') } export interface SendEmailParams { to: string | string[]; cc?: string | string[]; subject: string; text?: string; html?: string; inReplyTo?: string; references?: string[]; attachments?: EmailAttachment[]; contractId?: number; // Vertrag dem die gesendete E-Mail zugeordnet wird } // Anhang-Speicher-Ziele export interface AttachmentTargetSlot { key: string; label: string; field: string; hasDocument: boolean; currentPath?: string; } export interface AttachmentEntityWithSlots { id: number; label: string; slots: AttachmentTargetSlot[]; } export interface AttachmentTargetsResponse { customer: { id: number; name: string; type: 'PRIVATE' | 'BUSINESS'; slots: AttachmentTargetSlot[]; }; identityDocuments: AttachmentEntityWithSlots[]; bankCards: AttachmentEntityWithSlots[]; contract?: { id: number; contractNumber: string; type: string; energyDetailsId?: number; slots: AttachmentTargetSlot[]; }; } export const stressfreiEmailApi = { getByCustomer: async (customerId: number, includeInactive = false) => { const res = await api.get>(`/customers/${customerId}/stressfrei-emails`, { params: { includeInactive } }); return res.data; }, create: async (customerId: number, data: { email: string; platform?: string; notes?: string; provisionAtProvider?: boolean; createMailbox?: boolean; }) => { const res = await api.post>(`/customers/${customerId}/stressfrei-emails`, data); return res.data; }, update: async (id: number, data: Partial) => { const res = await api.put>(`/stressfrei-emails/${id}`, data); return res.data; }, delete: async (id: number) => { const res = await api.delete>(`/stressfrei-emails/${id}`); return res.data; }, // Mailbox nachträglich aktivieren enableMailbox: async (id: number) => { const res = await api.post>(`/stressfrei-emails/${id}/enable-mailbox`); return res.data; }, // Mailbox-Status mit Provider synchronisieren syncMailboxStatus: async (id: number) => { const res = await api.post>(`/stressfrei-emails/${id}/sync-mailbox-status`); return res.data; }, // Mailbox-Zugangsdaten abrufen (IMAP/SMTP) getMailboxCredentials: async (id: number) => { const res = await api.get>(`/stressfrei-emails/${id}/credentials`); return res.data; }, // Passwort zurücksetzen (generiert neues Passwort beim Provider) resetPassword: async (id: number) => { const res = await api.post>(`/stressfrei-emails/${id}/reset-password`); return res.data; }, // Weiterleitungen neu setzen (z.B. nach Änderung der Kunden-Stamm-E-Mail). // Wenn die Adresse hasMailbox=true ist, wird zusätzlich das im CRM // hinterlegte Passwort am Provider neu gesetzt (Self-Healing). syncForwarding: async (id: number) => { const res = await api.post>(`/stressfrei-emails/${id}/sync-forwarding`); return res.data; }, // E-Mails synchronisieren syncEmails: async (id: number, fullSync = false) => { const res = await api.post>(`/stressfrei-emails/${id}/sync`, {}, { params: { full: fullSync } }); return res.data; }, // E-Mail senden sendEmail: async (id: number, params: SendEmailParams) => { const res = await api.post>(`/stressfrei-emails/${id}/send`, params); return res.data; }, // Ordner-Anzahlen abrufen (total und ungelesen pro Ordner) getFolderCounts: async (id: number) => { const res = await api.get>(`/stressfrei-emails/${id}/folder-counts`); return res.data; }, }; // Cached Email API (E-Mail-Client) export interface EmailFilterParams { accountId?: number; folder?: 'INBOX' | 'SENT'; limit?: number; offset?: number; // Suche / Filter (alle AND-verknüpft) search?: string; fromFilter?: string; toFilter?: string; subjectFilter?: string; bodyFilter?: string; attachmentNameFilter?: string; hasAttachments?: boolean; isRead?: boolean; isStarred?: boolean; receivedFrom?: string; // ISO date receivedTo?: string; // ISO date } export const cachedEmailApi = { // E-Mails für Kunden abrufen getForCustomer: async (customerId: number, options?: EmailFilterParams) => { const res = await api.get>(`/customers/${customerId}/emails`, { params: options }); return res.data; }, // E-Mails für Vertrag abrufen getForContract: async (contractId: number, options?: { folder?: 'INBOX' | 'SENT'; limit?: number; offset?: number }) => { const res = await api.get>(`/contracts/${contractId}/emails`, { params: options }); return res.data; }, // Ordner-Anzahlen für Vertrag abrufen (zugeordnete E-Mails) getContractFolderCounts: async (contractId: number) => { const res = await api.get>(`/contracts/${contractId}/emails/folder-counts`); return res.data; }, // Mailbox-Konten eines Kunden abrufen getMailboxAccounts: async (customerId: number) => { const res = await api.get>(`/customers/${customerId}/mailbox-accounts`); return res.data; }, // Einzelne E-Mail abrufen (mit Body) getById: async (id: number) => { const res = await api.get>(`/emails/${id}`); return res.data; }, // E-Mail-Thread abrufen getThread: async (id: number) => { const res = await api.get>(`/emails/${id}/thread`); return res.data; }, // Als gelesen/ungelesen markieren markAsRead: async (id: number, isRead: boolean) => { const res = await api.patch>(`/emails/${id}/read`, { isRead }); return res.data; }, // Stern umschalten toggleStar: async (id: number) => { const res = await api.post>(`/emails/${id}/star`); return res.data; }, // Vertrag zuordnen assignToContract: async (emailId: number, contractId: number) => { const res = await api.post>(`/emails/${emailId}/assign`, { contractId }); return res.data; }, // Zuordnung aufheben unassignFromContract: async (emailId: number) => { const res = await api.delete>(`/emails/${emailId}/assign`); return res.data; }, // E-Mail löschen (nur Admin) delete: async (emailId: number) => { const res = await api.delete>(`/emails/${emailId}`); return res.data; }, // Anhang-URL (view=true für inline anzeigen, sonst download) // Hinweis: gibt die URL mit dem aktuellen Access-Token als Query-Param zurück, // weil