Files
opencrm/backend/src/lib/prisma.ts
T
duffyduck 5ffd1a4d2c fix: prisma.ts baut DATABASE_URL aus DB_*-Vars (für docker exec)
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>
2026-05-18 15:57:15 +02:00

156 lines
4.8 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 { 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;