Files
opencrm/backend/src/routes/developer.routes.ts
T
2026-03-21 18:23:54 +01:00

513 lines
17 KiB
TypeScript

import { Router, Response } from 'express';
import { Prisma } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { authenticate, requirePermission } from '../middleware/auth.js';
import { AuthRequest } from '../types/index.js';
const router = Router();
// Setup-Endpunkt: Erstellt die developer:access Permission und fügt sie der Admin-Rolle hinzu
// Dieser Endpunkt erfordert keine Authentifizierung, da er nur einmalig zum Setup verwendet wird
router.post('/setup', async (req, res: Response) => {
try {
// Create or get the developer:access permission
const developerPerm = await prisma.permission.upsert({
where: { resource_action: { resource: 'developer', action: 'access' } },
update: {},
create: { resource: 'developer', action: 'access' },
});
// Get the Admin role
const adminRole = await prisma.role.findUnique({
where: { name: 'Admin' },
include: { permissions: true },
});
if (!adminRole) {
res.status(404).json({ success: false, error: 'Admin-Rolle nicht gefunden' });
return;
}
// Check if Admin already has this permission
const hasPermission = adminRole.permissions.some(
(rp) => rp.permissionId === developerPerm.id
);
if (!hasPermission) {
await prisma.rolePermission.create({
data: {
roleId: adminRole.id,
permissionId: developerPerm.id,
},
});
res.json({ success: true, message: 'developer:access Permission wurde zur Admin-Rolle hinzugefügt. Bitte neu einloggen!' });
} else {
res.json({ success: true, message: 'Admin-Rolle hat bereits die developer:access Permission' });
}
} catch (error) {
console.error('Setup error:', error);
res.status(500).json({ success: false, error: 'Fehler beim Setup' });
}
});
// Tabellen-Metadaten mit Beziehungen
const tableMetadata: Record<string, {
model: string;
primaryKey: string;
readonlyFields: string[];
requiredFields: string[];
relations: { field: string; targetTable: string; type: 'one' | 'many' }[];
foreignKeys: { field: string; targetTable: string }[];
}> = {
User: {
model: 'user',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt', 'password'],
requiredFields: ['email', 'firstName', 'lastName'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'roles', targetTable: 'UserRole', type: 'many' },
],
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
},
Role: {
model: 'role',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['name'],
relations: [
{ field: 'permissions', targetTable: 'RolePermission', type: 'many' },
{ field: 'users', targetTable: 'UserRole', type: 'many' },
],
foreignKeys: [],
},
Permission: {
model: 'permission',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['resource', 'action'],
relations: [{ field: 'roles', targetTable: 'RolePermission', type: 'many' }],
foreignKeys: [],
},
RolePermission: {
model: 'rolePermission',
primaryKey: 'roleId,permissionId',
readonlyFields: [],
requiredFields: ['roleId', 'permissionId'],
relations: [],
foreignKeys: [
{ field: 'roleId', targetTable: 'Role' },
{ field: 'permissionId', targetTable: 'Permission' },
],
},
UserRole: {
model: 'userRole',
primaryKey: 'userId,roleId',
readonlyFields: [],
requiredFields: ['userId', 'roleId'],
relations: [],
foreignKeys: [
{ field: 'userId', targetTable: 'User' },
{ field: 'roleId', targetTable: 'Role' },
],
},
Customer: {
model: 'customer',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt', 'customerNumber'],
requiredFields: ['firstName', 'lastName'],
relations: [
{ field: 'user', targetTable: 'User', type: 'one' },
{ field: 'addresses', targetTable: 'Address', type: 'many' },
{ field: 'bankCards', targetTable: 'BankCard', type: 'many' },
{ field: 'identityDocuments', targetTable: 'IdentityDocument', type: 'many' },
{ field: 'meters', targetTable: 'Meter', type: 'many' },
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
],
foreignKeys: [],
},
Address: {
model: 'address',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['customerId', 'street', 'houseNumber', 'postalCode', 'city'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
],
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
},
BankCard: {
model: 'bankCard',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['customerId', 'accountHolder', 'iban'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
],
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
},
IdentityDocument: {
model: 'identityDocument',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['customerId', 'documentNumber'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
],
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
},
Meter: {
model: 'meter',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['customerId', 'meterNumber', 'type'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'readings', targetTable: 'MeterReading', type: 'many' },
{ field: 'energyDetails', targetTable: 'EnergyContractDetails', type: 'many' },
],
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
},
MeterReading: {
model: 'meterReading',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt'],
requiredFields: ['meterId', 'readingDate', 'value'],
relations: [{ field: 'meter', targetTable: 'Meter', type: 'one' }],
foreignKeys: [{ field: 'meterId', targetTable: 'Meter' }],
},
SalesPlatform: {
model: 'salesPlatform',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt'],
requiredFields: ['name'],
relations: [{ field: 'contracts', targetTable: 'Contract', type: 'many' }],
foreignKeys: [],
},
Contract: {
model: 'contract',
primaryKey: 'id',
readonlyFields: ['id', 'createdAt', 'updatedAt', 'contractNumber'],
requiredFields: ['customerId', 'type'],
relations: [
{ field: 'customer', targetTable: 'Customer', type: 'one' },
{ field: 'address', targetTable: 'Address', type: 'one' },
{ field: 'bankCard', targetTable: 'BankCard', type: 'one' },
{ field: 'identityDocument', targetTable: 'IdentityDocument', type: 'one' },
{ field: 'salesPlatform', targetTable: 'SalesPlatform', type: 'one' },
{ field: 'previousContract', targetTable: 'Contract', type: 'one' },
{ field: 'followUpContract', targetTable: 'Contract', type: 'one' },
{ field: 'energyDetails', targetTable: 'EnergyContractDetails', type: 'one' },
{ field: 'internetDetails', targetTable: 'InternetContractDetails', type: 'one' },
{ field: 'mobileDetails', targetTable: 'MobileContractDetails', type: 'one' },
{ field: 'tvDetails', targetTable: 'TvContractDetails', type: 'one' },
{ field: 'carInsuranceDetails', targetTable: 'CarInsuranceDetails', type: 'one' },
],
foreignKeys: [
{ field: 'customerId', targetTable: 'Customer' },
{ field: 'addressId', targetTable: 'Address' },
{ field: 'bankCardId', targetTable: 'BankCard' },
{ field: 'identityDocumentId', targetTable: 'IdentityDocument' },
{ field: 'salesPlatformId', targetTable: 'SalesPlatform' },
{ field: 'previousContractId', targetTable: 'Contract' },
],
},
EnergyContractDetails: {
model: 'energyContractDetails',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['contractId'],
relations: [
{ field: 'contract', targetTable: 'Contract', type: 'one' },
{ field: 'meter', targetTable: 'Meter', type: 'one' },
],
foreignKeys: [
{ field: 'contractId', targetTable: 'Contract' },
{ field: 'meterId', targetTable: 'Meter' },
],
},
InternetContractDetails: {
model: 'internetContractDetails',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['contractId'],
relations: [
{ field: 'contract', targetTable: 'Contract', type: 'one' },
{ field: 'phoneNumbers', targetTable: 'PhoneNumber', type: 'many' },
],
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
},
PhoneNumber: {
model: 'phoneNumber',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['internetContractDetailsId', 'phoneNumber'],
relations: [{ field: 'internetDetails', targetTable: 'InternetContractDetails', type: 'one' }],
foreignKeys: [{ field: 'internetContractDetailsId', targetTable: 'InternetContractDetails' }],
},
MobileContractDetails: {
model: 'mobileContractDetails',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['contractId'],
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
},
TvContractDetails: {
model: 'tvContractDetails',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['contractId'],
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
},
CarInsuranceDetails: {
model: 'carInsuranceDetails',
primaryKey: 'id',
readonlyFields: ['id'],
requiredFields: ['contractId'],
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
},
};
// Schema-Informationen abrufen
router.get(
'/schema',
authenticate,
requirePermission('developer:access'),
async (req: AuthRequest, res: Response) => {
try {
const tables = Object.entries(tableMetadata).map(([name, meta]) => ({
name,
...meta,
}));
res.json({ success: true, data: tables });
} catch (error) {
console.error('Schema error:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden des Schemas' });
}
}
);
// Tabellen-Daten abrufen
router.get(
'/table/:tableName',
authenticate,
requirePermission('developer:access'),
async (req: AuthRequest, res: Response) => {
try {
const { tableName } = req.params;
const { page = '1', limit = '50' } = req.query;
const meta = tableMetadata[tableName];
if (!meta) {
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
return;
}
const skip = (parseInt(page as string) - 1) * parseInt(limit as string);
const take = parseInt(limit as string);
const model = (prisma as any)[meta.model];
const [data, total] = await Promise.all([
model.findMany({
skip,
take,
orderBy: meta.primaryKey.includes(',') ? undefined : { [meta.primaryKey.split(',')[0]]: 'desc' },
}),
model.count(),
]);
res.json({
success: true,
data,
meta: {
...meta,
tableName,
},
pagination: {
page: parseInt(page as string),
limit: parseInt(limit as string),
total,
totalPages: Math.ceil(total / parseInt(limit as string)),
},
});
} catch (error) {
console.error('Table data error:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden der Daten' });
}
}
);
// Einzelne Zeile aktualisieren
router.put(
'/table/:tableName/:id',
authenticate,
requirePermission('developer:access'),
async (req: AuthRequest, res: Response) => {
try {
const { tableName, id } = req.params;
const updates = req.body;
const meta = tableMetadata[tableName];
if (!meta) {
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
return;
}
// Readonly-Felder aus Updates entfernen
const filteredUpdates: Record<string, any> = {};
for (const [key, value] of Object.entries(updates)) {
if (!meta.readonlyFields.includes(key)) {
filteredUpdates[key] = value;
}
}
// Prüfen ob required-Felder nicht auf null/leer gesetzt werden
for (const field of meta.requiredFields) {
if (field in filteredUpdates && (filteredUpdates[field] === null || filteredUpdates[field] === '')) {
res.status(400).json({ success: false, error: `Feld '${field}' ist erforderlich` });
return;
}
}
const model = (prisma as any)[meta.model];
// Composite Primary Key Handling
let where: any;
if (meta.primaryKey.includes(',')) {
const keys = meta.primaryKey.split(',');
const idParts = id.split('-');
where = {};
keys.forEach((key, idx) => {
where[key] = parseInt(idParts[idx]);
});
} else {
where = { [meta.primaryKey]: parseInt(id) };
}
const updated = await model.update({
where,
data: filteredUpdates,
});
res.json({ success: true, data: updated });
} catch (error: any) {
console.error('Update error:', error);
if (error.code === 'P2003') {
res.status(400).json({ success: false, error: 'Fremdschlüssel-Verletzung: Referenzierter Datensatz existiert nicht' });
} else if (error.code === 'P2002') {
res.status(400).json({ success: false, error: 'Unique-Constraint-Verletzung: Wert existiert bereits' });
} else {
res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
}
}
}
);
// Zeile löschen (nur wenn keine abhängigen Daten)
router.delete(
'/table/:tableName/:id',
authenticate,
requirePermission('developer:access'),
async (req: AuthRequest, res: Response) => {
try {
const { tableName, id } = req.params;
const meta = tableMetadata[tableName];
if (!meta) {
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
return;
}
const model = (prisma as any)[meta.model];
// Composite Primary Key Handling
let where: any;
if (meta.primaryKey.includes(',')) {
const keys = meta.primaryKey.split(',');
const idParts = id.split('-');
where = {};
keys.forEach((key, idx) => {
where[key] = parseInt(idParts[idx]);
});
} else {
where = { [meta.primaryKey]: parseInt(id) };
}
// Prüfen ob abhängige Daten existieren (nur "many"-Relations)
const record = await model.findUnique({
where,
include: meta.relations
.filter((r) => r.type === 'many')
.reduce((acc, r) => ({ ...acc, [r.field]: { take: 1 } }), {}),
});
if (!record) {
res.status(404).json({ success: false, error: 'Datensatz nicht gefunden' });
return;
}
// Prüfen auf abhängige Daten
for (const rel of meta.relations.filter((r) => r.type === 'many')) {
if (record[rel.field] && record[rel.field].length > 0) {
res.status(400).json({
success: false,
error: `Kann nicht gelöscht werden: Es existieren abhängige Daten in '${rel.targetTable}'`,
});
return;
}
}
await model.delete({ where });
res.json({ success: true });
} catch (error: any) {
console.error('Delete error:', error);
if (error.code === 'P2003') {
res.status(400).json({ success: false, error: 'Kann nicht gelöscht werden: Fremdschlüssel-Abhängigkeit' });
} else {
res.status(500).json({ success: false, error: 'Fehler beim Löschen' });
}
}
}
);
// Referenzierte Daten für Dropdowns abrufen
router.get(
'/reference/:tableName',
authenticate,
requirePermission('developer:access'),
async (req: AuthRequest, res: Response) => {
try {
const { tableName } = req.params;
const { search = '', limit = '50' } = req.query;
const meta = tableMetadata[tableName];
if (!meta) {
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
return;
}
const model = (prisma as any)[meta.model];
const data = await model.findMany({
take: parseInt(limit as string),
});
res.json({ success: true, data });
} catch (error) {
console.error('Reference error:', error);
res.status(500).json({ success: false, error: 'Fehler beim Laden der Referenzdaten' });
}
}
);
export default router;