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:
+30
-27
@@ -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()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
-8
@@ -1,20 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Open_Sans, Poppins } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import "./globals.css";
|
||||
|
||||
// Self-hosted über next/font (kein Google-CDN-Aufruf zur Laufzeit, DSGVO)
|
||||
const openSans = Open_Sans({
|
||||
// Selbst gehostet über next/font/local (committete woff2 in ./fonts) — kein
|
||||
// Google-Fonts-Fetch zur Build- oder Laufzeit. DSGVO-konform und macht den
|
||||
// Produktiv-Build reproduzierbar/offline (next/font/google lud die Schriften
|
||||
// sonst zur Build-Zeit von Google, was flakig fehlschlagen kann).
|
||||
const openSans = localFont({
|
||||
variable: "--font-open-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "600", "700"],
|
||||
display: "swap",
|
||||
src: [
|
||||
{ path: "./fonts/open-sans-300.woff2", weight: "300", style: "normal" },
|
||||
{ path: "./fonts/open-sans-400.woff2", weight: "400", style: "normal" },
|
||||
{ path: "./fonts/open-sans-600.woff2", weight: "600", style: "normal" },
|
||||
{ path: "./fonts/open-sans-700.woff2", weight: "700", style: "normal" },
|
||||
],
|
||||
});
|
||||
|
||||
const poppins = Poppins({
|
||||
const poppins = localFont({
|
||||
variable: "--font-poppins",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "500", "600", "700"],
|
||||
display: "swap",
|
||||
src: [
|
||||
{ path: "./fonts/poppins-300.woff2", weight: "300", style: "normal" },
|
||||
{ path: "./fonts/poppins-500.woff2", weight: "500", style: "normal" },
|
||||
{ path: "./fonts/poppins-600.woff2", weight: "600", style: "normal" },
|
||||
{ path: "./fonts/poppins-700.woff2", weight: "700", style: "normal" },
|
||||
],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
Reference in New Issue
Block a user