Files
craftvia/src/server/provision.ts
T
msolarczekandClaude Opus 5 8491c7f173 Fundament: ISMS-Module entfernt; Craftvia-Rollen, Module, Navigation, i18n-Split
- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds,
  Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity,
  MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin)
- Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email)
- TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt
- RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions
- Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports,
  emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts
- Modul-Routen mit requireModule-Layout und Platzhalterseite
- Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader
- check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/
- Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt
- Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/
  NotificationPreference-Fixtures umgestellt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:35:44 +02:00

102 lines
3.8 KiB
TypeScript

import type { PrismaClient } from "@prisma/client";
import { hashPassword } from "@/server/password";
import { PERMISSIONS, ROLE_DEFS } from "@/server/rbac";
import { MODULES } from "@/lib/modules";
export interface ProvisionOpts {
name: string;
slug: string;
short?: string;
sector?: string;
admin: { email: string; name: string; password: string };
actorId?: string | null;
}
/**
* Legt einen Mandanten idempotent an bzw. aktualisiert ihn: globaler Permission-Katalog,
* Standard-Rollen (ROLE_DEFS) inkl. Rechte, erster Mandantenadministrator (Identity +
* Mitgliedschaft), Mandanten-Einstellungen, alle Module aktiv, Audit-Eintrag.
*
* Andockpunkt Fachmodule: mandantenspezifische Stammdaten-Defaults (z. B. Auftragsarten,
* Checklisten-Vorlagen) werden künftig hier nach Schritt 6 angelegt.
*/
export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts) {
// 1. Globaler Permission-Katalog
for (const key of PERMISSIONS) {
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
}
// 2. Mandant
const tenant = await prisma.tenant.upsert({
where: { slug: opts.slug },
update: { name: opts.name, short: opts.short ?? null, sector: opts.sector ?? null },
create: { name: opts.name, slug: opts.slug, short: opts.short ?? null, sector: opts.sector ?? null },
});
// 3. Rollen + Permissions
for (const [key, def] of Object.entries(ROLE_DEFS)) {
const role = await prisma.role.upsert({
where: { tenantId_key: { tenantId: tenant.id, key } },
update: { name: def.name },
create: { tenantId: tenant.id, key, name: def.name },
});
const perms = await prisma.permission.findMany({ where: { key: { in: [...def.permissions] } } });
for (const p of perms) {
await prisma.rolePermission.upsert({
where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } },
update: {},
create: { roleId: role.id, permissionId: p.id },
});
}
}
// 4. Erster Mandantenadministrator: globale Identity (Anmeldung) + Mitgliedschaft.
// Idempotent: eine bestehende Identity/ihr Passwort wird NICHT überschrieben.
const passwordHash = await hashPassword(opts.admin.password);
const identity = await prisma.identity.upsert({
where: { email: opts.admin.email },
update: {},
create: { email: opts.admin.email, passwordHash },
});
const admin = await prisma.user.upsert({
where: { tenantId_email: { tenantId: tenant.id, email: opts.admin.email } },
update: { name: opts.admin.name, identityId: identity.id },
create: { tenantId: tenant.id, identityId: identity.id, email: opts.admin.email, name: opts.admin.name },
});
const adminRole = await prisma.role.findUnique({ where: { tenantId_key: { tenantId: tenant.id, key: "tenant-admin" } } });
if (adminRole) {
await prisma.userRole.upsert({
where: { userId_roleId: { userId: admin.id, roleId: adminRole.id } },
update: {},
create: { userId: admin.id, roleId: adminRole.id },
});
}
// 5. Mandanten-Einstellungen (Unternehmensdaten)
await prisma.tenantSettings.upsert({
where: { tenantId: tenant.id },
update: {},
create: {
tenantId: tenant.id,
orgName: opts.name,
orgShort: opts.short ?? null,
sector: opts.sector ?? null,
},
});
// 6. Alle Module aktivieren
for (const m of MODULES) {
await prisma.tenantModule.upsert({
where: { tenantId_moduleKey: { tenantId: tenant.id, moduleKey: m.key } },
update: {},
create: { tenantId: tenant.id, moduleKey: m.key, enabled: true },
});
}
await prisma.auditLog.create({
data: { tenantId: tenant.id, scope: "platform", actorId: opts.actorId ?? null, action: "provision", entity: "tenant", entityId: tenant.id, after: { name: opts.name } },
});
return tenant;
}