5ffd1a4d2c
docker-compose reicht DB_USER/DB_PASSWORD/DB_HOST/DB_NAME an den Container weiter, aber DATABASE_URL wird erst beim Container-Start im entrypoint.sh aus diesen Komponenten zusammengebaut und exportiert. `docker exec` startet eine neue Shell, die das exportierte DATABASE_URL nicht erbt → ./scripts/admin-rescue.sh brach mit "Environment variable not found: DATABASE_URL" ab. src/lib/prisma.ts macht jetzt dieselbe URL-Konstruktion einmal zentral. Damit funktionieren alle Wartungsskripte (reset-admin- password, cleanup-xss-and-mass-assignment) bei docker exec ohne Wrapper-Hack. Server-Start ist unbeeinflusst (DATABASE_URL ist da schon gesetzt). Live-verifiziert lokal: env -u DATABASE_URL DB_USER=... npx tsx prisma/reset-admin-password.ts admin@admin.com → success. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
import { PrismaClient, Prisma } from '@prisma/client';
|
||
import { setBeforeValues, setAfterValues } from '../middleware/auditContext.js';
|
||
|
||
// DATABASE_URL aus DB_*-Komponenten bauen, falls nicht explizit gesetzt.
|
||
// Der entrypoint.sh macht das ebenfalls (für den Server-Start). Aber bei
|
||
// `docker exec opencrm-app npx tsx prisma/<script>.ts` läuft eine neue
|
||
// Shell ohne diese exportierte Variable – die DB_*-Vars sind aus dem
|
||
// docker-compose.yml vererbt, DATABASE_URL aber nicht. Damit alle
|
||
// Wartungsskripte (reset-admin-password, cleanup-xss-...) und Server
|
||
// dieselbe Logik nutzen, machen wir es einmal zentral hier.
|
||
if (!process.env.DATABASE_URL && process.env.DB_USER && process.env.DB_PASSWORD && process.env.DB_NAME) {
|
||
const u = encodeURIComponent(process.env.DB_USER);
|
||
const p = encodeURIComponent(process.env.DB_PASSWORD);
|
||
const h = process.env.DB_HOST || 'db';
|
||
const port = process.env.DB_PORT || '3306';
|
||
const n = process.env.DB_NAME;
|
||
process.env.DATABASE_URL = `mysql://${u}:${p}@${h}:${port}/${n}`;
|
||
}
|
||
|
||
// Modelle die für Before/After-Tracking relevant sind
|
||
const AUDITED_MODELS = [
|
||
'Customer',
|
||
'Contract',
|
||
'Address',
|
||
'BankCard',
|
||
'IdentityDocument',
|
||
'User',
|
||
'Meter',
|
||
'MeterReading',
|
||
'StressfreiEmail',
|
||
'Provider',
|
||
'Tariff',
|
||
'ContractCategory',
|
||
'AppSetting',
|
||
'CustomerConsent',
|
||
'EnergyContractDetails',
|
||
'RepresentativeAuthorization',
|
||
'ContractMeter',
|
||
'EmailProviderConfig',
|
||
'ContractTask',
|
||
];
|
||
|
||
// Sensible Felder die aus dem Audit-Log gefiltert werden
|
||
const SENSITIVE_FIELDS = [
|
||
'password',
|
||
'passwordHash',
|
||
'portalPasswordHash',
|
||
'portalPasswordEncrypted',
|
||
'emailPasswordEncrypted',
|
||
'internetPasswordEncrypted',
|
||
'sipPasswordEncrypted',
|
||
'pin',
|
||
'puk',
|
||
'apiKey',
|
||
];
|
||
|
||
/**
|
||
* Filtert sensible Felder aus einem Objekt
|
||
*/
|
||
function filterSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {
|
||
const filtered: Record<string, unknown> = {};
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
if (SENSITIVE_FIELDS.includes(key)) {
|
||
filtered[key] = '[REDACTED]';
|
||
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||
filtered[key] = filterSensitiveFields(value as Record<string, unknown>);
|
||
} else {
|
||
filtered[key] = value;
|
||
}
|
||
}
|
||
return filtered;
|
||
}
|
||
|
||
/**
|
||
* Prüft ob ein Model für Audit-Tracking relevant ist
|
||
*/
|
||
function isAuditedModel(model: string | undefined): boolean {
|
||
return model !== undefined && AUDITED_MODELS.includes(model);
|
||
}
|
||
|
||
/**
|
||
* Erstellt einen Prisma Client mit Audit-Middleware
|
||
*/
|
||
function createPrismaClient(): PrismaClient {
|
||
const prisma = new PrismaClient();
|
||
|
||
// Middleware für Before/After-Tracking
|
||
prisma.$use(async (params: Prisma.MiddlewareParams, next: (params: Prisma.MiddlewareParams) => Promise<unknown>) => {
|
||
const { model, action, args } = params;
|
||
|
||
// Nur relevante Modelle und Aktionen tracken
|
||
if (!isAuditedModel(model)) {
|
||
return next(params);
|
||
}
|
||
|
||
// Update-Operationen: Vorherigen Stand abrufen
|
||
if (action === 'update' || action === 'updateMany') {
|
||
try {
|
||
const modelDelegate = (prisma as unknown as Record<string, { findUnique: (args: unknown) => Promise<unknown> }>)[
|
||
model!.charAt(0).toLowerCase() + model!.slice(1)
|
||
];
|
||
|
||
if (modelDelegate && args?.where) {
|
||
const before = await modelDelegate.findUnique({ where: args.where });
|
||
if (before) {
|
||
setBeforeValues(filterSensitiveFields(before as Record<string, unknown>));
|
||
}
|
||
}
|
||
} catch {
|
||
// Fehler beim Abrufen des vorherigen Stands ignorieren
|
||
}
|
||
}
|
||
|
||
// Delete-Operationen: Datensatz vor dem Löschen abrufen
|
||
if (action === 'delete' || action === 'deleteMany') {
|
||
try {
|
||
const modelDelegate = (prisma as unknown as Record<string, { findUnique: (args: unknown) => Promise<unknown> }>)[
|
||
model!.charAt(0).toLowerCase() + model!.slice(1)
|
||
];
|
||
|
||
if (modelDelegate && args?.where) {
|
||
const before = await modelDelegate.findUnique({ where: args.where });
|
||
if (before) {
|
||
setBeforeValues(filterSensitiveFields(before as Record<string, unknown>));
|
||
}
|
||
}
|
||
} catch {
|
||
// Fehler beim Abrufen ignorieren
|
||
}
|
||
}
|
||
|
||
// Operation ausführen
|
||
const result = await next(params);
|
||
|
||
// Nach Update/Create: Neuen Stand speichern
|
||
if ((action === 'update' || action === 'create') && result) {
|
||
setAfterValues(filterSensitiveFields(result as Record<string, unknown>));
|
||
}
|
||
|
||
return result;
|
||
});
|
||
|
||
return prisma;
|
||
}
|
||
|
||
// Singleton-Instanz
|
||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
|
||
|
||
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
globalForPrisma.prisma = prisma;
|
||
}
|
||
|
||
export default prisma;
|