8534be22d0
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>
1875 lines
72 KiB
TypeScript
1875 lines
72 KiB
TypeScript
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;
|