diff --git a/scripts/bootstrap-admin.ts b/scripts/bootstrap-admin.ts index d6118ec..8ab3866 100644 --- a/scripts/bootstrap-admin.ts +++ b/scripts/bootstrap-admin.ts @@ -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() diff --git a/src/app/fonts/open-sans-300.woff2 b/src/app/fonts/open-sans-300.woff2 new file mode 100644 index 0000000..d6c8549 Binary files /dev/null and b/src/app/fonts/open-sans-300.woff2 differ diff --git a/src/app/fonts/open-sans-400.woff2 b/src/app/fonts/open-sans-400.woff2 new file mode 100644 index 0000000..e2d3fa4 Binary files /dev/null and b/src/app/fonts/open-sans-400.woff2 differ diff --git a/src/app/fonts/open-sans-600.woff2 b/src/app/fonts/open-sans-600.woff2 new file mode 100644 index 0000000..f48dda7 Binary files /dev/null and b/src/app/fonts/open-sans-600.woff2 differ diff --git a/src/app/fonts/open-sans-700.woff2 b/src/app/fonts/open-sans-700.woff2 new file mode 100644 index 0000000..f5977a4 Binary files /dev/null and b/src/app/fonts/open-sans-700.woff2 differ diff --git a/src/app/fonts/poppins-300.woff2 b/src/app/fonts/poppins-300.woff2 new file mode 100644 index 0000000..962b734 Binary files /dev/null and b/src/app/fonts/poppins-300.woff2 differ diff --git a/src/app/fonts/poppins-500.woff2 b/src/app/fonts/poppins-500.woff2 new file mode 100644 index 0000000..c660336 Binary files /dev/null and b/src/app/fonts/poppins-500.woff2 differ diff --git a/src/app/fonts/poppins-600.woff2 b/src/app/fonts/poppins-600.woff2 new file mode 100644 index 0000000..921e962 Binary files /dev/null and b/src/app/fonts/poppins-600.woff2 differ diff --git a/src/app/fonts/poppins-700.woff2 b/src/app/fonts/poppins-700.woff2 new file mode 100644 index 0000000..bf022fc Binary files /dev/null and b/src/app/fonts/poppins-700.woff2 differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c79e4b9..0bacc63 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 = {