Fix: Fonts selbst hosten (next/font/local) + Bootstrap an Superadmin-Store anpassen

- next/font/google lud Open Sans/Poppins zur Build-Zeit von Google -> flakiger
  'Failed to fetch from Google Fonts' bricht den Prod-Build ab. Jetzt committete
  woff2 (src/app/fonts) via next/font/local -> reproduzierbarer Offline-Build, DSGVO.
- scripts/bootstrap-admin.ts: provisionTenant nimmt kein isPlatformAdmin mehr; der
  Superadmin ist jetzt das eigene Modell platformAdmin (Login /platform/login, MFA).
  Bootstrap legt Mandant+Mandanten-Admin UND platformAdmin an (idempotent).
- node:22-Docker-Build lokal verifiziert (kompiliert + TypeCheck + Routen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 22:32:52 +02:00
co-authored by Claude Opus 4.8
parent cc8d48698f
commit f368d8fadd
10 changed files with 51 additions and 35 deletions
+30 -27
View File
@@ -1,28 +1,30 @@
import "dotenv/config";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { hash } from "@node-rs/argon2";
import { provisionTenant } from "@/server/provision";
/**
* Erst-Bootstrap für Produktiv: legt den ersten Plattform-Admin + einen Mandanten an
* — als Ersatz für den Demo-Seed (der NUR für Test/Dev ist). Ohne diesen Schritt gibt
* es in Prod keinen Benutzer und niemand kann sich anmelden.
* Erst-Bootstrap für Produktiv (Ersatz für den Demo-Seed, der nur für Test/Dev ist).
* Ohne diesen Schritt gibt es in Prod keinen Zugang.
*
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per BOOTSTRAP_ADMIN=true
* (siehe docker-compose.coolify.yml und docs/DEPLOY-PROD-CONTABO.md).
* Legt an:
* 1. Ersten Mandanten + Mandanten-Admin → Login unter /login
* 2. Plattform-Admin (Superadmin, eigener Store) → Login unter /platform/login (MFA
* wird beim ersten Login eingerichtet)
* Beide mit denselben Zugangsdaten (BOOTSTRAP_ADMIN_*), analog zum Seed.
*
* Idempotent: existiert bereits ein Plattform-Admin mit der E-Mail, passiert nichts
* (kein Passwort-Reset, keine Duplikate). provisionTenant selbst arbeitet mit upserts.
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per
* BOOTSTRAP_ADMIN=true (siehe docker-compose.coolify.yml, docs/DEPLOY-PROD-CONTABO.md).
*
* Idempotent: provisionTenant und der platformAdmin.upsert nutzen upserts; ein bereits
* gesetztes Passwort wird beim erneuten Lauf NICHT überschrieben.
*
* Erforderliche Umgebungsvariablen:
* BOOTSTRAP_ADMIN_EMAIL E-Mail des ersten Plattform-Admins
* BOOTSTRAP_ADMIN_PASSWORD Initiales Passwort (nach erstem Login ändern)
* BOOTSTRAP_ADMIN_NAME Anzeigename
* BOOTSTRAP_TENANT_NAME Name des ersten Mandanten
* BOOTSTRAP_TENANT_SLUG URL-Slug des Mandanten (klein, eindeutig)
* BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_NAME
* BOOTSTRAP_TENANT_NAME, BOOTSTRAP_TENANT_SLUG
* Optional:
* BOOTSTRAP_TENANT_SHORT Kürzel des Mandanten
* BOOTSTRAP_TENANT_SECTOR Branche
* BOOTSTRAP_TENANT_SHORT, BOOTSTRAP_TENANT_SECTOR
*/
const prisma = new PrismaClient({
@@ -47,28 +49,29 @@ async function main() {
const short = process.env.BOOTSTRAP_TENANT_SHORT?.trim() || undefined;
const sector = process.env.BOOTSTRAP_TENANT_SECTOR?.trim() || undefined;
// Idempotenz: existiert der Plattform-Admin schon, nichts tun (kein PW-Reset).
const existing = await prisma.user.findFirst({
where: { email, isPlatformAdmin: true },
select: { id: true },
});
if (existing) {
console.log(`✔ Plattform-Admin ${email} existiert bereits — Bootstrap übersprungen.`);
return;
}
// 1. Mandant + Mandanten-Admin (Zugang /login). Idempotent (upserts).
const tenant = await provisionTenant(prisma, {
name: tenantName,
slug: tenantSlug,
short,
sector,
admin: { email, name, password, isPlatformAdmin: true },
admin: { email, name, password },
});
// 2. Plattform-Admin / Superadmin (Zugang /platform/login, MFA beim ersten Login).
// upsert: Passwort nur bei Neuanlage setzen — bei erneutem Lauf nicht zurücksetzen.
await prisma.platformAdmin.upsert({
where: { email },
update: { name, status: "ACTIVE" },
create: { email, name, passwordHash: await hash(password), status: "ACTIVE" },
});
console.log(
`✔ Bootstrap fertig: Mandant "${tenant.name}" (${tenantSlug}) + Plattform-Admin ${email} angelegt.`
`✔ Bootstrap fertig: Mandant "${tenant.name}" (${tenantSlug}), Mandanten-Admin + Plattform-Admin ${email} angelegt.`
);
console.log(
" → /platform/login = Superadmin (MFA beim ersten Login), /login = Mandanten-Zugang. Passwort nach erstem Login ändern."
);
console.log(" → Bitte nach dem ersten Login das Passwort ändern.");
}
main()