Files
opencrm/frontend/src/services/api.ts
T
duffyduck 8534be22d0 Einmalpasswort-Flow für Portal-Credentials
Wenn Admin "Zugangsdaten versenden" klickt, ist das Passwort jetzt ein
echtes Einmalpasswort: beim ersten erfolgreichen Portal-Login werden
Hash + Encrypted-Feld sofort genullt und der Kunde wird zwangsweise
auf eine "Neues Passwort vergeben"-Seite geleitet. Erst nach eigenem
Passwort kommt er ins Portal.

Schema:
- Customer.portalPasswordMustChange: Boolean @default(false)

Backend:
- sendPortalCredentials setzt Flag = true + erweitertes Mail-Template
  mit Einmalpasswort-Warnung
- customerLogin: bei Flag=true wird OTP konsumiert (Hash+Encrypted=null,
  portalLastLogin aktualisiert), Response enthält mustChangePassword=true
  in token-payload + user-objekt
- setCustomerPortalPassword (manuelles Setzen) räumt Flag wieder auf
- changeInitialPortalPassword: neue Service-Funktion + Endpoint
  POST /api/auth/change-initial-portal-password (authenticated, nur
  Portal-User), validiert Komplexität, setzt neuen Hash, löscht
  Encrypted, invalidiert Session via portalTokenInvalidatedAt

Frontend:
- User-Type erweitert um mustChangePassword
- AuthContext.customerLogin gibt User zurück (für sofortige Routing-
  Entscheidung)
- Login.tsx: redirect zu /change-initial-password wenn mustChangePassword
- ProtectedRoute: zwingt eingeloggte User mit Flag immer zur Change-Seite
- ChangeInitialPasswordGate: blockt User OHNE Flag vom Zugriff
- ChangeInitialPassword: eigene Seite mit Live-Komplexitäts-Hint,
  Passwort-Wiederholung, automatischer Logout + Redirect nach Erfolg

Live-verifiziert (10 Schritte):
- Setzen → Send → DB-Flag=true → OTP-Login gibt mustChange=true und
  consumed Hash → Re-Login mit OTP fehlschlägt → Change schwach=400,
  komplex=200 → neues Passwort funktioniert → Session invalidated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:48:13 +02:00

1875 lines
72 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string | null> | null = null;
async function doRefresh(): Promise<string | null> {
if (refreshInflight) return refreshInflight;
refreshInflight = (async () => {
try {
const res = await axios.post<ApiResponse<{ token: string }>>(
'/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<ApiResponse<{ token: string; user: User }>>('/auth/login', { email, password });
return res.data;
},
customerLogin: async (email: string, password: string) => {
const res = await api.post<ApiResponse<{ token: string; user: User }>>('/auth/customer-login', { email, password });
return res.data;
},
me: async () => {
const res = await api.get<ApiResponse<User>>('/auth/me');
return res.data;
},
logout: async () => {
const res = await api.post<ApiResponse<void>>('/auth/logout');
return res.data;
},
changeInitialPortalPassword: async (newPassword: string) => {
const res = await api.post<ApiResponse<void>>('/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<ApiResponse<Customer[]>>('/customers', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Customer>>(`/customers/${id}`);
return res.data;
},
create: async (data: Partial<Customer>) => {
const res = await api.post<ApiResponse<Customer>>('/customers', data);
return res.data;
},
update: async (id: number, data: Partial<Customer>) => {
const res = await api.put<ApiResponse<Customer>>(`/customers/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/customers/${id}`);
return res.data;
},
// Portal-Einstellungen
getPortalSettings: async (customerId: number) => {
const res = await api.get<ApiResponse<PortalSettings>>(`/customers/${customerId}/portal`);
return res.data;
},
updatePortalSettings: async (customerId: number, data: { portalEnabled?: boolean; portalEmail?: string | null }) => {
const res = await api.put<ApiResponse<PortalSettings>>(`/customers/${customerId}/portal`, data);
return res.data;
},
setPortalPassword: async (customerId: number, password: string) => {
const res = await api.post<ApiResponse<void>>(`/customers/${customerId}/portal/password`, { password });
return res.data;
},
getPortalPassword: async (customerId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/customers/${customerId}/portal/password`);
return res.data;
},
generatePortalPassword: async (customerId: number) => {
const res = await api.post<ApiResponse<{ password: string }>>(`/customers/${customerId}/portal/password/generate`);
return res.data;
},
sendPortalCredentials: async (customerId: number) => {
const res = await api.post<ApiResponse<void>>(`/customers/${customerId}/portal/send-credentials`);
return res.data;
},
// Vertreter-Verwaltung
getRepresentatives: async (customerId: number) => {
const res = await api.get<ApiResponse<CustomerRepresentative[]>>(`/customers/${customerId}/representatives`);
return res.data;
},
addRepresentative: async (customerId: number, representativeId: number, notes?: string) => {
const res = await api.post<ApiResponse<CustomerRepresentative>>(`/customers/${customerId}/representatives`, { representativeId, notes });
return res.data;
},
removeRepresentative: async (customerId: number, representativeId: number) => {
const res = await api.delete<ApiResponse<void>>(`/customers/${customerId}/representatives/${representativeId}`);
return res.data;
},
searchForRepresentative: async (customerId: number, search: string) => {
const res = await api.get<ApiResponse<CustomerSummary[]>>(`/customers/${customerId}/representatives/search`, { params: { search } });
return res.data;
},
};
// Addresses
export const addressApi = {
getByCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<Address[]>>(`/customers/${customerId}/addresses`);
return res.data;
},
create: async (customerId: number, data: Partial<Address>) => {
const res = await api.post<ApiResponse<Address>>(`/customers/${customerId}/addresses`, data);
return res.data;
},
update: async (id: number, data: Partial<Address>) => {
const res = await api.put<ApiResponse<Address>>(`/addresses/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/addresses/${id}`);
return res.data;
},
};
// Bank Cards
export const bankCardApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<BankCard[]>>(`/customers/${customerId}/bank-cards`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<BankCard>) => {
const res = await api.post<ApiResponse<BankCard>>(`/customers/${customerId}/bank-cards`, data);
return res.data;
},
update: async (id: number, data: Partial<BankCard>) => {
const res = await api.put<ApiResponse<BankCard>>(`/bank-cards/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/bank-cards/${id}`);
return res.data;
},
};
// Identity Documents
export const documentApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<IdentityDocument[]>>(`/customers/${customerId}/documents`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<IdentityDocument>) => {
const res = await api.post<ApiResponse<IdentityDocument>>(`/customers/${customerId}/documents`, data);
return res.data;
},
update: async (id: number, data: Partial<IdentityDocument>) => {
const res = await api.put<ApiResponse<IdentityDocument>>(`/documents/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/documents/${id}`);
return res.data;
},
};
// Meters
export const meterApi = {
getByCustomer: async (customerId: number, showInactive = false) => {
const res = await api.get<ApiResponse<Meter[]>>(`/customers/${customerId}/meters`, { params: { showInactive } });
return res.data;
},
create: async (customerId: number, data: Partial<Meter>) => {
const res = await api.post<ApiResponse<Meter>>(`/customers/${customerId}/meters`, data);
return res.data;
},
update: async (id: number, data: Partial<Meter>) => {
const res = await api.put<ApiResponse<Meter>>(`/meters/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/meters/${id}`);
return res.data;
},
getReadings: async (meterId: number) => {
const res = await api.get<ApiResponse<MeterReading[]>>(`/meters/${meterId}/readings`);
return res.data;
},
addReading: async (meterId: number, data: Partial<MeterReading>) => {
const res = await api.post<ApiResponse<MeterReading>>(`/meters/${meterId}/readings`, data);
return res.data;
},
updateReading: async (meterId: number, readingId: number, data: Partial<MeterReading>) => {
const res = await api.put<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/${readingId}`, data);
return res.data;
},
deleteReading: async (meterId: number, readingId: number) => {
const res = await api.delete<ApiResponse<void>>(`/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<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/report`, data);
return res.data;
},
getMyMeters: async () => {
const res = await api.get<ApiResponse<Meter[]>>('/meters/my-meters');
return res.data;
},
// Status-Update
markTransferred: async (meterId: number, readingId: number) => {
const res = await api.patch<ApiResponse<MeterReading>>(`/meters/${meterId}/readings/${readingId}/transfer`);
return res.data;
},
};
// Invoice API
export const invoiceApi = {
getInvoices: async (ecdId: number) => {
const res = await api.get<ApiResponse<Invoice[]>>(`/energy-details/${ecdId}/invoices`);
return res.data;
},
addInvoice: async (ecdId: number, data: Partial<Invoice>) => {
const res = await api.post<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices`, data);
return res.data;
},
addInvoiceByContract: async (contractId: number, data: Partial<Invoice>) => {
const res = await api.post<ApiResponse<Invoice>>(`/contracts/${contractId}/invoices`, data);
return res.data;
},
updateInvoice: async (ecdId: number, invoiceId: number, data: Partial<Invoice>) => {
const res = await api.put<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data);
return res.data;
},
deleteInvoice: async (ecdId: number, invoiceId: number) => {
const res = await api.delete<ApiResponse<void>>(`/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<ApiResponse<{ path: string; filename: string }>>(
`/upload/invoices/${invoiceId}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return res.data;
},
deleteDocument: async (invoiceId: number) => {
const res = await api.delete<ApiResponse<void>>(`/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<ApiResponse<StressfreiEmail[]>>(`/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<ApiResponse<StressfreiEmail>>(`/customers/${customerId}/stressfrei-emails`, data);
return res.data;
},
update: async (id: number, data: Partial<StressfreiEmail>) => {
const res = await api.put<ApiResponse<StressfreiEmail>>(`/stressfrei-emails/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/stressfrei-emails/${id}`);
return res.data;
},
// Mailbox nachträglich aktivieren
enableMailbox: async (id: number) => {
const res = await api.post<ApiResponse<null>>(`/stressfrei-emails/${id}/enable-mailbox`);
return res.data;
},
// Mailbox-Status mit Provider synchronisieren
syncMailboxStatus: async (id: number) => {
const res = await api.post<ApiResponse<{ hasMailbox: boolean; wasUpdated: boolean }>>(`/stressfrei-emails/${id}/sync-mailbox-status`);
return res.data;
},
// Mailbox-Zugangsdaten abrufen (IMAP/SMTP)
getMailboxCredentials: async (id: number) => {
const res = await api.get<ApiResponse<{
email: string;
password: string;
imap: { server: string; port: number; encryption: string } | null;
smtp: { server: string; port: number; encryption: string } | null;
}>>(`/stressfrei-emails/${id}/credentials`);
return res.data;
},
// Passwort zurücksetzen (generiert neues Passwort beim Provider)
resetPassword: async (id: number) => {
const res = await api.post<ApiResponse<{ password: string }>>(`/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<ApiResponse<{
forwardTargets: string[];
customerEmail: string;
passwordReset?: boolean;
}>>(`/stressfrei-emails/${id}/sync-forwarding`);
return res.data;
},
// E-Mails synchronisieren
syncEmails: async (id: number, fullSync = false) => {
const res = await api.post<ApiResponse<SyncResult>>(`/stressfrei-emails/${id}/sync`, {}, { params: { full: fullSync } });
return res.data;
},
// E-Mail senden
sendEmail: async (id: number, params: SendEmailParams) => {
const res = await api.post<ApiResponse<{ messageId: string }>>(`/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<ApiResponse<{
inbox: number;
inboxUnread: number;
sent: number;
sentUnread: number;
trash: number;
trashUnread: number;
}>>(`/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<ApiResponse<CachedEmail[]>>(`/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<ApiResponse<CachedEmail[]>>(`/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<ApiResponse<{
inbox: number;
inboxUnread: number;
sent: number;
sentUnread: number;
}>>(`/contracts/${contractId}/emails/folder-counts`);
return res.data;
},
// Mailbox-Konten eines Kunden abrufen
getMailboxAccounts: async (customerId: number) => {
const res = await api.get<ApiResponse<MailboxAccount[]>>(`/customers/${customerId}/mailbox-accounts`);
return res.data;
},
// Einzelne E-Mail abrufen (mit Body)
getById: async (id: number) => {
const res = await api.get<ApiResponse<CachedEmail>>(`/emails/${id}`);
return res.data;
},
// E-Mail-Thread abrufen
getThread: async (id: number) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/emails/${id}/thread`);
return res.data;
},
// Als gelesen/ungelesen markieren
markAsRead: async (id: number, isRead: boolean) => {
const res = await api.patch<ApiResponse<void>>(`/emails/${id}/read`, { isRead });
return res.data;
},
// Stern umschalten
toggleStar: async (id: number) => {
const res = await api.post<ApiResponse<{ isStarred: boolean }>>(`/emails/${id}/star`);
return res.data;
},
// Vertrag zuordnen
assignToContract: async (emailId: number, contractId: number) => {
const res = await api.post<ApiResponse<CachedEmail>>(`/emails/${emailId}/assign`, { contractId });
return res.data;
},
// Zuordnung aufheben
unassignFromContract: async (emailId: number) => {
const res = await api.delete<ApiResponse<CachedEmail>>(`/emails/${emailId}/assign`);
return res.data;
},
// E-Mail löschen (nur Admin)
delete: async (emailId: number) => {
const res = await api.delete<ApiResponse<void>>(`/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 <iframe>/<a> keinen Authorization-Header senden können. Der Token läuft
// nach 15 min ab wenn Anhang dann geöffnet wird, kommt 401; UI muss in dem
// Fall die URL frisch holen.
getAttachmentUrl: (emailId: number, filename: string, view?: boolean) => {
const token = getAccessToken();
const encodedFilename = encodeURIComponent(filename);
const viewParam = view ? '&view=true' : '';
return `${api.defaults.baseURL}/emails/${emailId}/attachments/${encodedFilename}?token=${token || ''}${viewParam}`;
},
// Ungelesene E-Mails zählen
getUnreadCount: async (params: { customerId?: number; contractId?: number }) => {
const res = await api.get<ApiResponse<{ count: number }>>('/emails/unread-count', { params });
return res.data;
},
// ==================== PAPIERKORB ====================
// Papierkorb-E-Mails für Kunden abrufen
getTrash: async (customerId: number) => {
const res = await api.get<ApiResponse<CachedEmail[]>>(`/customers/${customerId}/emails/trash`);
return res.data;
},
// Papierkorb-Anzahl für Kunden
getTrashCount: async (customerId: number) => {
const res = await api.get<ApiResponse<{ count: number }>>(`/customers/${customerId}/emails/trash/count`);
return res.data;
},
// E-Mail aus Papierkorb wiederherstellen
restore: async (emailId: number) => {
const res = await api.post<ApiResponse<void>>(`/emails/${emailId}/restore`);
return res.data;
},
// E-Mail endgültig löschen (aus Papierkorb)
permanentDelete: async (emailId: number) => {
const res = await api.delete<ApiResponse<void>>(`/emails/${emailId}/permanent`);
return res.data;
},
// ==================== ANHANG-SPEICHERUNG ====================
// Verfügbare Dokumenten-Ziele für Anhänge abrufen
getAttachmentTargets: async (emailId: number) => {
const res = await api.get<ApiResponse<AttachmentTargetsResponse>>(`/emails/${emailId}/attachment-targets`);
return res.data;
},
// Anhang in Dokumentenfeld speichern
saveAttachmentTo: async (emailId: number, filename: string, params: { entityType: string; entityId?: number; targetKey: string }) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ path: string; filename: string; originalName: string; size: number }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-to`,
params
);
return res.data;
},
// E-Mail als PDF speichern
saveEmailAsPdf: async (emailId: number, params: { entityType: string; entityId?: number; targetKey: string }) => {
const res = await api.post<ApiResponse<{ path: string; filename: string; size: number }>>(
`/emails/${emailId}/save-as-pdf`,
params
);
return res.data;
},
// E-Mail als Rechnung speichern (für Energieverträge)
saveEmailAsInvoice: async (emailId: number, params: { invoiceDate: string; invoiceType: string; notes?: string }) => {
const res = await api.post<ApiResponse<{ id: number; invoiceDate: string; invoiceType: string; documentPath: string }>>(
`/emails/${emailId}/save-as-invoice`,
params
);
return res.data;
},
// Anhang als Rechnung speichern (für Energieverträge)
saveAttachmentAsInvoice: async (emailId: number, filename: string, params: { invoiceDate: string; invoiceType: string; notes?: string }) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ id: number; invoiceDate: string; invoiceType: string; documentPath: string }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-as-invoice`,
params
);
return res.data;
},
saveAttachmentAsContractDocument: async (
emailId: number,
filename: string,
params: { documentType: string; notes?: string; deliveryDate?: string },
) => {
const encodedFilename = encodeURIComponent(filename);
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
`/emails/${emailId}/attachments/${encodedFilename}/save-as-contract-document`,
params,
);
return res.data;
},
};
// Contracts - Vertragsbaum für Kundenansicht
export interface ContractTreeNodeContract {
id: number;
contractNumber: string;
type: string;
status: string;
startDate: string | null;
endDate: string | null;
providerName: string | null;
tariffName: string | null;
previousContractId: number | null;
provider?: { id: number; name: string } | null;
tariff?: { id: number; name: string } | null;
contractCategory?: { id: number; name: string } | null;
customer?: { id: number; firstName: string; lastName: string; companyName: string | null; customerNumber: string } | null;
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
simCards: { phoneNumber: string | null; isMain: boolean }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}
export interface ContractTreeNode {
contract: ContractTreeNodeContract;
predecessors: ContractTreeNode[];
hasHistory: boolean;
}
export const contractApi = {
getAll: async (params?: { customerId?: number; type?: string; status?: string; search?: string; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
return res.data;
},
getTreeForCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', { params: { customerId, tree: 'true' } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Contract>>(`/contracts/${id}`);
return res.data;
},
create: async (data: Partial<Contract> & { [key: string]: unknown }) => {
const res = await api.post<ApiResponse<Contract>>('/contracts', data);
return res.data;
},
update: async (id: number, data: Partial<Contract> & { [key: string]: unknown }) => {
const res = await api.put<ApiResponse<Contract>>(`/contracts/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${id}`);
return res.data;
},
createFollowUp: async (id: number) => {
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/follow-up`);
return res.data;
},
createRenewal: async (id: number) => {
const res = await api.post<ApiResponse<Contract>>(`/contracts/${id}/renewal`);
return res.data;
},
getPassword: async (id: number) => {
const res = await api.get<ApiResponse<{ password: string }>>(`/contracts/${id}/password`);
return res.data;
},
getSimCardCredentials: async (simCardId: number) => {
const res = await api.get<ApiResponse<{ pin: string | null; puk: string | null }>>(`/contracts/simcard/${simCardId}/credentials`);
return res.data;
},
getInternetCredentials: async (contractId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/contracts/${contractId}/internet-credentials`);
return res.data;
},
getSipCredentials: async (phoneNumberId: number) => {
const res = await api.get<ApiResponse<{ password: string | null }>>(`/contracts/phonenumber/${phoneNumberId}/sip-credentials`);
return res.data;
},
// Vertrags-Cockpit
getCockpit: async () => {
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
return res.data;
},
// Vertragsdokumente
getDocuments: async (contractId: number) => {
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
return res.data;
},
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string, deliveryDate?: string) => {
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', documentType);
if (notes) formData.append('notes', notes);
if (deliveryDate) formData.append('deliveryDate', deliveryDate);
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteDocument: async (contractId: number, documentId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/documents/${documentId}`);
return res.data;
},
// Folgezähler
addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => {
const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data);
return res.data;
},
removeContractMeter: async (contractId: number, contractMeterId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/contract-meter/${contractMeterId}`);
return res.data;
},
// Snooze: Vertrag zurückstellen
snooze: async (id: number, data: { nextReviewDate?: string; months?: number }) => {
const res = await api.patch<ApiResponse<{ id: number; contractNumber: string; nextReviewDate: string | null }>>(`/contracts/${id}/snooze`, data);
return res.data;
},
};
// Contract History (Vertragshistorie - nur intern)
export const contractHistoryApi = {
getByContract: async (contractId: number) => {
const res = await api.get<ApiResponse<ContractHistoryEntry[]>>(`/contracts/${contractId}/history`);
return res.data;
},
create: async (contractId: number, data: { title: string; description?: string }) => {
const res = await api.post<ApiResponse<ContractHistoryEntry>>(`/contracts/${contractId}/history`, data);
return res.data;
},
update: async (contractId: number, entryId: number, data: { title?: string; description?: string }) => {
const res = await api.put<ApiResponse<ContractHistoryEntry>>(`/contracts/${contractId}/history/${entryId}`, data);
return res.data;
},
delete: async (contractId: number, entryId: number) => {
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/history/${entryId}`);
return res.data;
},
};
// Contract Tasks (Aufgaben)
export const contractTaskApi = {
// Alle Tasks über alle Verträge (für Task-Liste & Dashboard)
getAll: async (params?: { status?: ContractTaskStatus; customerId?: number }) => {
const res = await api.get<ApiResponse<ContractTask[]>>('/tasks', { params });
return res.data;
},
// Task-Statistik (offene Aufgaben)
getStats: async () => {
const res = await api.get<ApiResponse<{ openCount: number }>>('/tasks/stats');
return res.data;
},
// Tasks für einen spezifischen Vertrag
getByContract: async (contractId: number, status?: ContractTaskStatus) => {
const res = await api.get<ApiResponse<ContractTask[]>>(`/contracts/${contractId}/tasks`, { params: { status } });
return res.data;
},
create: async (contractId: number, data: { title: string; description?: string; visibleInPortal?: boolean }) => {
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/tasks`, data);
return res.data;
},
update: async (taskId: number, data: { title?: string; description?: string; visibleInPortal?: boolean }) => {
const res = await api.put<ApiResponse<ContractTask>>(`/tasks/${taskId}`, data);
return res.data;
},
complete: async (taskId: number) => {
const res = await api.post<ApiResponse<ContractTask>>(`/tasks/${taskId}/complete`);
return res.data;
},
reopen: async (taskId: number) => {
const res = await api.post<ApiResponse<ContractTask>>(`/tasks/${taskId}/reopen`);
return res.data;
},
delete: async (taskId: number) => {
const res = await api.delete<ApiResponse<void>>(`/tasks/${taskId}`);
return res.data;
},
// Subtasks
createSubtask: async (taskId: number, title: string) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/tasks/${taskId}/subtasks`, { title });
return res.data;
},
// Kundenportal: Antwort auf eigenes Ticket
createReply: async (taskId: number, title: string) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/tasks/${taskId}/reply`, { title });
return res.data;
},
updateSubtask: async (subtaskId: number, title: string) => {
const res = await api.put<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}`, { title });
return res.data;
},
completeSubtask: async (subtaskId: number) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}/complete`);
return res.data;
},
reopenSubtask: async (subtaskId: number) => {
const res = await api.post<ApiResponse<ContractTaskSubtask>>(`/subtasks/${subtaskId}/reopen`);
return res.data;
},
deleteSubtask: async (subtaskId: number) => {
const res = await api.delete<ApiResponse<void>>(`/subtasks/${subtaskId}`);
return res.data;
},
// Support-Ticket erstellen (für Kundenportal)
createSupportTicket: async (contractId: number, data: { title: string; description?: string }) => {
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/support-ticket`, data);
return res.data;
},
};
// App Settings
export const appSettingsApi = {
getPublic: async () => {
const res = await api.get<ApiResponse<Record<string, string>>>('/settings/public');
return res.data;
},
getAll: async () => {
const res = await api.get<ApiResponse<Record<string, string>>>('/settings');
return res.data;
},
update: async (settings: Record<string, string>) => {
const res = await api.put<ApiResponse<void>>('/settings', settings);
return res.data;
},
updateOne: async (key: string, value: string) => {
const res = await api.put<ApiResponse<void>>(`/settings/${key}`, { value });
return res.data;
},
};
// Backup & Restore
export interface BackupInfo {
name: string;
timestamp: string;
totalRecords: number;
tables: { table: string; count: number }[];
sizeBytes: number;
hasUploads: boolean;
uploadSizeBytes: number;
}
export const backupApi = {
list: async () => {
const res = await api.get<ApiResponse<BackupInfo[]>>('/settings/backups');
return res.data;
},
create: async () => {
const res = await api.post<ApiResponse<{ backupName: string }>>('/settings/backup');
return res.data;
},
restore: async (name: string) => {
const res = await api.post<ApiResponse<{ restoredRecords: number; restoredFiles: number }>>(`/settings/backup/${name}/restore`);
return res.data;
},
delete: async (name: string) => {
const res = await api.delete<ApiResponse<void>>(`/settings/backup/${name}`);
return res.data;
},
getDownloadUrl: (name: string) => {
return `/api/settings/backup/${name}/download`;
},
upload: async (file: File) => {
const formData = new FormData();
formData.append('backup', file);
const res = await api.post<ApiResponse<{ backupName: string }>>('/settings/backup/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
factoryReset: async () => {
const res = await api.post<ApiResponse<void>>('/settings/factory-reset');
return res.data;
},
};
// Platforms
export const platformApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<SalesPlatform[]>>('/platforms', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<SalesPlatform>>(`/platforms/${id}`);
return res.data;
},
create: async (data: Partial<SalesPlatform>) => {
const res = await api.post<ApiResponse<SalesPlatform>>('/platforms', data);
return res.data;
},
update: async (id: number, data: Partial<SalesPlatform>) => {
const res = await api.put<ApiResponse<SalesPlatform>>(`/platforms/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/platforms/${id}`);
return res.data;
},
};
// Cancellation Periods
export const cancellationPeriodApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<CancellationPeriod[]>>('/cancellation-periods', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<CancellationPeriod>>(`/cancellation-periods/${id}`);
return res.data;
},
create: async (data: Partial<CancellationPeriod>) => {
const res = await api.post<ApiResponse<CancellationPeriod>>('/cancellation-periods', data);
return res.data;
},
update: async (id: number, data: Partial<CancellationPeriod>) => {
const res = await api.put<ApiResponse<CancellationPeriod>>(`/cancellation-periods/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/cancellation-periods/${id}`);
return res.data;
},
};
// Contract Durations
export const contractDurationApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<ContractDuration[]>>('/contract-durations', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<ContractDuration>>(`/contract-durations/${id}`);
return res.data;
},
create: async (data: Partial<ContractDuration>) => {
const res = await api.post<ApiResponse<ContractDuration>>('/contract-durations', data);
return res.data;
},
update: async (id: number, data: Partial<ContractDuration>) => {
const res = await api.put<ApiResponse<ContractDuration>>(`/contract-durations/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contract-durations/${id}`);
return res.data;
},
};
// Contract Categories (Vertragstypen)
export const contractCategoryApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<ContractCategory[]>>('/contract-categories', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<ContractCategory>>(`/contract-categories/${id}`);
return res.data;
},
create: async (data: Partial<ContractCategory>) => {
const res = await api.post<ApiResponse<ContractCategory>>('/contract-categories', data);
return res.data;
},
update: async (id: number, data: Partial<ContractCategory>) => {
const res = await api.put<ApiResponse<ContractCategory>>(`/contract-categories/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/contract-categories/${id}`);
return res.data;
},
};
// Providers (Anbieter)
export const providerApi = {
getAll: async (includeInactive = false) => {
const res = await api.get<ApiResponse<Provider[]>>('/providers', { params: { includeInactive } });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<Provider>>(`/providers/${id}`);
return res.data;
},
create: async (data: Partial<Provider>) => {
const res = await api.post<ApiResponse<Provider>>('/providers', data);
return res.data;
},
update: async (id: number, data: Partial<Provider>) => {
const res = await api.put<ApiResponse<Provider>>(`/providers/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/providers/${id}`);
return res.data;
},
getTariffs: async (providerId: number, includeInactive = false) => {
const res = await api.get<ApiResponse<Tariff[]>>(`/providers/${providerId}/tariffs`, { params: { includeInactive } });
return res.data;
},
createTariff: async (providerId: number, data: Partial<Tariff>) => {
const res = await api.post<ApiResponse<Tariff>>(`/providers/${providerId}/tariffs`, data);
return res.data;
},
};
// Tariffs (Tarife)
export const tariffApi = {
getById: async (id: number) => {
const res = await api.get<ApiResponse<Tariff>>(`/tariffs/${id}`);
return res.data;
},
update: async (id: number, data: Partial<Tariff>) => {
const res = await api.put<ApiResponse<Tariff>>(`/tariffs/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/tariffs/${id}`);
return res.data;
},
};
// Upload
export const uploadApi = {
uploadBankCardDocument: async (bankCardId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/bank-cards/${bankCardId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
uploadIdentityDocument: async (documentId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/documents/${documentId}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteBankCardDocument: async (bankCardId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/bank-cards/${bankCardId}`);
return res.data;
},
deleteIdentityDocument: async (documentId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/documents/${documentId}`);
return res.data;
},
uploadBusinessRegistration: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/business-registration`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteBusinessRegistration: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/business-registration`);
return res.data;
},
uploadCommercialRegister: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/commercial-register`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCommercialRegister: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/commercial-register`);
return res.data;
},
uploadPrivacyPolicy: async (customerId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/customers/${customerId}/privacy-policy`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deletePrivacyPolicy: async (customerId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/customers/${customerId}/privacy-policy`);
return res.data;
},
// Contract Documents
uploadCancellationLetter: async (contractId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-letter`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationLetter: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter`);
return res.data;
},
uploadCancellationConfirmation: async (contractId: number, file: File, confirmationDate?: string) => {
const formData = new FormData();
formData.append('document', file);
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationConfirmation: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-confirmation`);
return res.data;
},
uploadCancellationLetterOptions: async (contractId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-letter-options`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationLetterOptions: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter-options`);
return res.data;
},
uploadCancellationConfirmationOptions: async (contractId: number, file: File, confirmationDate?: string) => {
const formData = new FormData();
formData.append('document', file);
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
deleteCancellationConfirmationOptions: async (contractId: number) => {
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`);
return res.data;
},
};
// Users
export const userApi = {
getAll: async (params?: { search?: string; isActive?: boolean; roleId?: number; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<User[]>>('/users', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<User>>(`/users/${id}`);
return res.data;
},
create: async (data: { email: string; password: string; firstName: string; lastName: string; roleIds: number[]; customerId?: number; hasDeveloperAccess?: boolean; hasGdprAccess?: boolean; whatsappNumber?: string; telegramUsername?: string; signalNumber?: string }) => {
const res = await api.post<ApiResponse<User>>('/users', data);
return res.data;
},
update: async (id: number, data: Partial<User> & { password?: string; roleIds?: number[] }) => {
const res = await api.put<ApiResponse<User>>(`/users/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/users/${id}`);
return res.data;
},
getRoles: async () => {
const res = await api.get<ApiResponse<Role[]>>('/users/roles/list');
return res.data;
},
};
// Developer
export const developerApi = {
getSchema: async () => {
const res = await api.get<ApiResponse<any[]>>('/developer/schema');
return res.data;
},
getTableData: async (tableName: string, page = 1, limit = 50) => {
const res = await api.get<ApiResponse<any[]>>(`/developer/table/${tableName}`, { params: { page, limit } });
return res.data;
},
updateRow: async (tableName: string, id: string, data: Record<string, any>) => {
const res = await api.put<ApiResponse<any>>(`/developer/table/${tableName}/${id}`, data);
return res.data;
},
deleteRow: async (tableName: string, id: string) => {
const res = await api.delete<ApiResponse<void>>(`/developer/table/${tableName}/${id}`);
return res.data;
},
getReference: async (tableName: string) => {
const res = await api.get<ApiResponse<any[]>>(`/developer/reference/${tableName}`);
return res.data;
},
};
// Email Provider (für Stressfrei-Wechseln Provisionierung)
export interface EmailProviderConfig {
id: number;
name: string;
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
passwordEncrypted?: string;
domain: string;
defaultForwardEmail?: string;
// IMAP/SMTP-Server (für E-Mail-Client)
imapServer?: string;
imapPort?: number;
smtpServer?: string;
smtpPort?: number;
// Verschlüsselungs-Einstellungen
imapEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
smtpEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
// System-E-Mail für automatisierten Versand
systemEmailAddress?: string;
systemEmailPasswordEncrypted?: string;
// UI-Label für Kunden-E-Mail-Adressen (z.B. "Stressfrei-Wechseln")
customerEmailLabel?: string | null;
isActive: boolean;
isDefault: boolean;
createdAt: string;
updatedAt: string;
}
export interface EmailOperationResult {
success: boolean;
message?: string;
error?: string;
}
export const emailProviderApi = {
// Config CRUD
getConfigs: async () => {
const res = await api.get<ApiResponse<EmailProviderConfig[]>>('/email-providers/configs');
return res.data;
},
getConfig: async (id: number) => {
const res = await api.get<ApiResponse<EmailProviderConfig>>(`/email-providers/configs/${id}`);
return res.data;
},
createConfig: async (data: Partial<EmailProviderConfig> & { password?: string }) => {
const res = await api.post<ApiResponse<EmailProviderConfig>>('/email-providers/configs', data);
return res.data;
},
updateConfig: async (id: number, data: Partial<EmailProviderConfig> & { password?: string }) => {
const res = await api.put<ApiResponse<EmailProviderConfig>>(`/email-providers/configs/${id}`, data);
return res.data;
},
deleteConfig: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/email-providers/configs/${id}`);
return res.data;
},
// Email Operations
testConnection: async (options?: {
id?: number;
testData?: {
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
apiUrl: string;
apiKey?: string;
username?: string;
password?: string;
domain: string;
};
}) => {
const body = options?.testData
? { ...options.testData }
: options?.id
? { id: options.id }
: {};
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/test-connection', body);
return res.data;
},
testMailAccess: async (body: {
id?: number;
apiUrl?: string;
domain?: string;
systemEmailAddress?: string;
systemEmailPassword?: string;
imapEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
smtpEncryption?: 'SSL' | 'STARTTLS' | 'NONE';
allowSelfSignedCerts?: boolean;
}) => {
const res = await api.post<ApiResponse<{
imap: { success: boolean; error?: string; server: string; port: number; encryption: string };
smtp: { success: boolean; error?: string; server: string; port: number; encryption: string };
user: string;
}>>('/email-providers/test-mail-access', body);
return res.data;
},
getDomain: async () => {
const res = await api.get<ApiResponse<{ domain: string | null }>>('/email-providers/domain');
return res.data;
},
getPublicSettings: async () => {
const res = await api.get<ApiResponse<{
domain: string | null;
customerEmailLabel: string;
customerEmailLabelIsCustom: boolean;
}>>('/email-providers/public-settings');
return res.data;
},
checkEmailExists: async (localPart: string) => {
const res = await api.get<ApiResponse<{ exists: boolean; email?: string }>>(`/email-providers/check/${localPart}`);
return res.data;
},
provisionEmail: async (localPart: string, customerEmail: string) => {
const res = await api.post<ApiResponse<EmailOperationResult>>('/email-providers/provision', { localPart, customerEmail });
return res.data;
},
deprovisionEmail: async (localPart: string) => {
const res = await api.delete<ApiResponse<EmailOperationResult>>(`/email-providers/deprovision/${localPart}`);
return res.data;
},
};
// ==================== AUDIT-LOGS ====================
export interface AuditLogSearchParams {
page?: number;
limit?: number;
userId?: number;
action?: string;
resourceType?: string;
sensitivity?: AuditSensitivity;
dataSubjectId?: number;
startDate?: string;
endDate?: string;
search?: string;
}
export const auditLogApi = {
search: async (params?: AuditLogSearchParams) => {
const res = await api.get<ApiResponse<AuditLog[]>>('/audit-logs', { params });
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<AuditLog>>(`/audit-logs/${id}`);
return res.data;
},
getByCustomer: async (customerId: number) => {
const res = await api.get<ApiResponse<AuditLog[]>>(`/audit-logs/customer/${customerId}`);
return res.data;
},
export: async (params?: AuditLogSearchParams & { format?: 'json' | 'csv' }) => {
const res = await api.get<ApiResponse<{ data: AuditLog[]; format: string }>>('/audit-logs/export', { params });
return res.data;
},
verifyIntegrity: async () => {
const res = await api.post<ApiResponse<{ valid: boolean; checkedCount: number; invalidEntries: number[]; message: string }>>('/audit-logs/verify');
return res.data;
},
rehash: async () => {
const res = await api.post<ApiResponse<{ rehashedCount: number }>>('/audit-logs/rehash');
return res.data;
},
getRetentionPolicies: async () => {
const res = await api.get<ApiResponse<AuditRetentionPolicy[]>>('/audit-logs/retention-policies');
return res.data;
},
updateRetentionPolicy: async (id: number, data: { retentionDays: number; isActive?: boolean }) => {
const res = await api.put<ApiResponse<AuditRetentionPolicy>>(`/audit-logs/retention-policies/${id}`, data);
return res.data;
},
runRetentionCleanup: async () => {
const res = await api.post<ApiResponse<{ deletedCount: number }>>('/audit-logs/cleanup');
return res.data;
},
};
// ==================== PDF TEMPLATES ====================
export const pdfTemplateApi = {
getAll: async () => {
const res = await api.get<ApiResponse<import('../types').PdfTemplate[]>>('/pdf-templates');
return res.data;
},
getById: async (id: number) => {
const res = await api.get<ApiResponse<import('../types').PdfTemplate>>(`/pdf-templates/${id}`);
return res.data;
},
create: async (formData: FormData) => {
const res = await api.post<ApiResponse<import('../types').PdfTemplate & { pdfFields?: import('../types').PdfField[] }>>('/pdf-templates', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
},
update: async (id: number, data: Record<string, unknown>) => {
const res = await api.put<ApiResponse<import('../types').PdfTemplate>>(`/pdf-templates/${id}`, data);
return res.data;
},
delete: async (id: number) => {
const res = await api.delete<ApiResponse<void>>(`/pdf-templates/${id}`);
return res.data;
},
getFields: async (id: number) => {
const res = await api.get<ApiResponse<import('../types').PdfField[]>>(`/pdf-templates/${id}/fields`);
return res.data;
},
getCrmFields: async (maxPhoneFields?: number) => {
const res = await api.get<ApiResponse<import('../types').CrmField[]>>('/pdf-templates/crm-fields', { params: { maxPhoneFields } });
return res.data;
},
getRequiredInputs: async (templateId: number, contractId: number) => {
const res = await api.get<ApiResponse<{
needsStressfreiEmail: boolean;
stressfreiEmails: { id: number; email: string }[];
manualFields: { key: string; pdfFieldName: string }[];
}>>(`/pdf-templates/${templateId}/generate/${contractId}/inputs`);
return res.data;
},
generatePdf: async (templateId: number, contractId: number, extras?: { stressfreiEmailId?: number; manualValues?: Record<string, string> }) => {
const res = await api.post(`/pdf-templates/${templateId}/generate/${contractId}`, extras || {}, { responseType: 'blob' });
return res.data;
},
generateUrl: (templateId: number, contractId: number) =>
`/api/pdf-templates/${templateId}/generate/${contractId}`,
};
// ==================== EMAIL LOG ====================
export interface EmailLog {
id: number;
fromAddress: string;
toAddress: string;
subject: string;
context: string;
customerId?: number;
triggeredBy?: string;
smtpServer: string;
smtpPort: number;
smtpEncryption: string;
smtpUser: string;
success: boolean;
messageId?: string;
errorMessage?: string;
smtpResponse?: string;
sentAt: string;
}
// ==================== MONITORING ====================
export type SecurityEventType =
| 'LOGIN_FAILED' | 'LOGIN_SUCCESS' | 'RATE_LIMIT_HIT' | 'ACCESS_DENIED'
| 'SSRF_BLOCKED' | 'PASSWORD_RESET_REQUEST' | 'PASSWORD_RESET_CONFIRM'
| 'LOGOUT' | 'TOKEN_REJECTED' | 'PERMISSION_CHANGED' | 'SUSPICIOUS';
export type SecuritySeverity = 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
export interface SecurityEvent {
id: number;
type: SecurityEventType;
severity: SecuritySeverity;
message: string;
ipAddress: string | null;
userId: number | null;
customerId: number | null;
userEmail: string | null;
endpoint: string | null;
details: Record<string, unknown> | null;
alerted: boolean;
alertedAt: string | null;
createdAt: string;
}
export interface MonitoringSettings {
alertEmail: string;
digestEnabled: boolean;
lastDigestAt: string | null;
}
export const monitoringApi = {
getEvents: async (params?: { page?: number; limit?: number; type?: SecurityEventType | ''; severity?: SecuritySeverity | ''; search?: string; ip?: string; since?: string }) => {
const q = new URLSearchParams();
if (params?.page) q.set('page', String(params.page));
if (params?.limit) q.set('limit', String(params.limit));
if (params?.type) q.set('type', params.type);
if (params?.severity) q.set('severity', params.severity);
if (params?.search) q.set('search', params.search);
if (params?.ip) q.set('ip', params.ip);
if (params?.since) q.set('since', params.since);
const res = await api.get<ApiResponse<SecurityEvent[]> & {
pagination: { page: number; limit: number; total: number; totalPages: number };
stats: { byType: Record<string, number>; bySeverity: Record<string, number> };
}>(`/monitoring/events?${q}`);
return res.data;
},
getSettings: async () => {
const res = await api.get<ApiResponse<MonitoringSettings>>('/monitoring/settings');
return res.data;
},
updateSettings: async (data: { alertEmail?: string; digestEnabled?: boolean }) => {
const res = await api.put<ApiResponse<void>>('/monitoring/settings', data);
return res.data;
},
testAlert: async () => {
const res = await api.post<ApiResponse<void>>('/monitoring/test-alert');
return res.data;
},
runDigest: async () => {
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
return res.data;
},
clearEvents: async (olderThanDays?: number) => {
const q = olderThanDays ? `?olderThanDays=${olderThanDays}` : '';
const res = await api.delete<ApiResponse<{ deletedCount: number }>>(`/monitoring/events${q}`);
return res.data;
},
};
export const emailLogApi = {
getLogs: async (params?: { page?: number; limit?: number; success?: string; search?: string; context?: string }) => {
const query = new URLSearchParams();
if (params?.page) query.set('page', params.page.toString());
if (params?.limit) query.set('limit', params.limit.toString());
if (params?.success !== undefined) query.set('success', params.success);
if (params?.search) query.set('search', params.search);
if (params?.context) query.set('context', params.context);
const res = await api.get<ApiResponse<EmailLog[]> & { pagination: { page: number; limit: number; total: number; totalPages: number } }>(`/email-logs?${query}`);
return res.data;
},
getStats: async () => {
const res = await api.get<ApiResponse<{ total: number; success: number; failed: number; last24h: number }>>('/email-logs/stats');
return res.data;
},
getDetail: async (id: number) => {
const res = await api.get<ApiResponse<EmailLog>>(`/email-logs/${id}`);
return res.data;
},
};
// ==================== DSGVO ====================
export const gdprApi = {
// Dashboard
getDashboardStats: async () => {
const res = await api.get<ApiResponse<GDPRDashboardStats>>('/gdpr/dashboard');
return res.data;
},
// Datenexport (Art. 15)
exportCustomerData: async (customerId: number) => {
const res = await api.get<ApiResponse<unknown>>(`/gdpr/customer/${customerId}/export`);
return res.data;
},
// Löschanfragen
getDeletionRequests: async (params?: { status?: DeletionRequestStatus; page?: number; limit?: number }) => {
const res = await api.get<ApiResponse<DataDeletionRequest[]>>('/gdpr/deletions', { params });
return res.data;
},
getDeletionRequest: async (id: number) => {
const res = await api.get<ApiResponse<DataDeletionRequest>>(`/gdpr/deletions/${id}`);
return res.data;
},
createDeletionRequest: async (data: { customerId: number; requestSource: string; requestedBy: string }) => {
const res = await api.post<ApiResponse<DataDeletionRequest>>('/gdpr/deletions', data);
return res.data;
},
processDeletionRequest: async (id: number, data: { processedBy: string; action: 'complete' | 'partial' | 'reject'; retentionReason?: string }) => {
const res = await api.put<ApiResponse<DataDeletionRequest>>(`/gdpr/deletions/${id}/process`, data);
return res.data;
},
// Consent-Status prüfen (hat Kunde vollständig zugestimmt?)
checkConsentStatus: async (customerId: number) => {
const res = await api.get<ApiResponse<{ hasConsent: boolean; hasPaperConsent: boolean; hasOnlineConsent: boolean; consentDetails: { type: string; status: string }[]; consentHash: string | null }>>(`/gdpr/customer/${customerId}/consent-status`);
return res.data;
},
getMyConsentStatus: async () => {
const res = await api.get<ApiResponse<{ hasConsent: boolean; hasPaperConsent: boolean; hasOnlineConsent: boolean; consentDetails: { type: string; status: string }[]; consentHash: string | null }>>('/gdpr/my-consent-status');
return res.data;
},
// Einwilligungen
getCustomerConsents: async (customerId: number) => {
const res = await api.get<ApiResponse<CustomerConsent[]>>(`/gdpr/customer/${customerId}/consents`);
return res.data;
},
updateConsent: async (customerId: number, consentType: ConsentType, data: { status: ConsentStatus; source?: string }) => {
const res = await api.put<ApiResponse<CustomerConsent>>(`/gdpr/customer/${customerId}/consents/${consentType}`, data);
return res.data;
},
getConsentOverview: async () => {
const res = await api.get<ApiResponse<Record<string, { granted: number; withdrawn: number; pending: number }>>>('/gdpr/consents/overview');
return res.data;
},
// Datenschutzerklärung (Editor)
getPrivacyPolicy: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/privacy-policy');
return res.data;
},
updatePrivacyPolicy: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/privacy-policy', { html });
return res.data;
},
// Vollmacht-Vorlage
getAuthorizationTemplate: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/authorization-template');
return res.data;
},
updateAuthorizationTemplate: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/authorization-template', { html });
return res.data;
},
// Impressum
getImprint: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/imprint');
return res.data;
},
updateImprint: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/imprint', { html });
return res.data;
},
// Website-Datenschutzerklärung
getWebsitePrivacyPolicy: async () => {
const res = await api.get<ApiResponse<{ html: string }>>('/gdpr/website-privacy-policy');
return res.data;
},
updateWebsitePrivacyPolicy: async (html: string) => {
const res = await api.put<ApiResponse<void>>('/gdpr/website-privacy-policy', { html });
return res.data;
},
// Consent-Link senden
sendConsentLink: async (customerId: number, channel: string) => {
const res = await api.post<ApiResponse<{ url: string; channel: string; hash: string }>>(`/gdpr/customer/${customerId}/send-consent-link`, { channel });
return res.data;
},
// Portal: Eigene Datenschutzseite
getMyPrivacy: async () => {
const res = await api.get<ApiResponse<{ privacyPolicyHtml: string; consents: CustomerConsent[] }>>('/gdpr/my-privacy');
return res.data;
},
getMyPrivacyPdfUrl: '/api/gdpr/my-privacy/pdf',
// Vollmachten (Admin)
getAuthorizations: async (customerId: number) => {
const res = await api.get<ApiResponse<RepresentativeAuthorization[]>>(`/gdpr/customer/${customerId}/authorizations`);
return res.data;
},
sendAuthorizationRequest: async (customerId: number, representativeId: number, channel: string) => {
const res = await api.post<ApiResponse<{ channel: string; portalUrl: string; messageText: string }>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/send`, { channel });
return res.data;
},
grantAuthorization: async (customerId: number, representativeId: number, data?: { source?: string; notes?: string }) => {
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/grant`, data || {});
return res.data;
},
withdrawAuthorization: async (customerId: number, representativeId: number) => {
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/withdraw`);
return res.data;
},
uploadAuthorizationDocument: async (customerId: number, representativeId: number, file: File) => {
const formData = new FormData();
formData.append('document', file);
const res = await api.post<ApiResponse<RepresentativeAuthorization>>(
`/gdpr/customer/${customerId}/authorizations/${representativeId}/upload`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return res.data;
},
deleteAuthorizationDocument: async (customerId: number, representativeId: number) => {
const res = await api.delete<ApiResponse<RepresentativeAuthorization>>(`/gdpr/customer/${customerId}/authorizations/${representativeId}/document`);
return res.data;
},
// Vollmachten (Portal)
getMyAuthorizations: async () => {
const res = await api.get<ApiResponse<RepresentativeAuthorization[]>>('/gdpr/my-authorizations');
return res.data;
},
toggleMyAuthorization: async (representativeId: number, grant: boolean) => {
const res = await api.put<ApiResponse<RepresentativeAuthorization>>(`/gdpr/my-authorizations/${representativeId}`, { grant });
return res.data;
},
getMyAuthorizationStatus: async () => {
const res = await api.get<ApiResponse<{ customerId: number; hasAuthorization: boolean }[]>>('/gdpr/my-authorization-status');
return res.data;
},
};
// ==================== PUBLIC API (kein Auth-Token) ====================
const publicAxios = axios.create({
baseURL: '/api/public',
headers: { 'Content-Type': 'application/json' },
});
export const publicApi = {
getConsentPage: async (hash: string) => {
const res = await publicAxios.get<ApiResponse<{
customer: { firstName: string; lastName: string; customerNumber: string };
consents: Array<{
consentType: string;
status: string;
label: string;
description: string;
grantedAt: string | null;
}>;
privacyPolicyHtml: string;
}>>(`/consent/${hash}`);
return res.data;
},
grantAllConsents: async (hash: string) => {
const res = await publicAxios.post<ApiResponse<void>>(`/consent/${hash}/grant`);
return res.data;
},
getConsentPdfUrl: (hash: string) => `/api/public/consent/${hash}/pdf`,
};
// ============ BIRTHDAY API ============
export interface BirthdayEntry {
customerId: number;
customerNumber: string;
name: string;
birthDate: string;
age: number;
daysUntil: number;
isToday: boolean;
isPast: boolean;
portalEnabled: boolean;
email?: string | null;
phone?: string | null;
}
export interface MyBirthdayCheck {
show: boolean;
isToday: boolean;
daysAgo: number;
firstName: string;
lastName: string;
salutation: string | null;
useInformalAddress: boolean;
age: number;
}
export const birthdayApi = {
getUpcoming: async (past: number = 7, future: number = 30) => {
const res = await api.get<ApiResponse<BirthdayEntry[]>>('/birthdays/upcoming', {
params: { past, future },
});
return res.data;
},
getMyBirthday: async () => {
const res = await api.get<ApiResponse<MyBirthdayCheck | null>>('/birthdays/my-birthday');
return res.data;
},
acknowledgeMyBirthday: async () => {
const res = await api.post<ApiResponse<void>>('/birthdays/my-birthday/acknowledge');
return res.data;
},
resetGreeting: async (customerId: number) => {
const res = await api.post<ApiResponse<void>>(`/birthdays/${customerId}/reset`);
return res.data;
},
sendGreeting: async (customerId: number, channel: 'email' | 'whatsapp' | 'telegram' | 'signal') => {
const res = await api.post<ApiResponse<{ channel: string; messageText: string }>>(
`/birthdays/${customerId}/send`,
{ channel },
);
return res.data;
},
};
export default api;