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 "dotenv/config";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { hash } from "@node-rs/argon2";
|
||||||
import { provisionTenant } from "@/server/provision";
|
import { provisionTenant } from "@/server/provision";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erst-Bootstrap für Produktiv: legt den ersten Plattform-Admin + einen Mandanten an
|
* Erst-Bootstrap für Produktiv (Ersatz für den Demo-Seed, der nur für Test/Dev ist).
|
||||||
* — als Ersatz für den Demo-Seed (der NUR für Test/Dev ist). Ohne diesen Schritt gibt
|
* Ohne diesen Schritt gibt es in Prod keinen Zugang.
|
||||||
* es in Prod keinen Benutzer und niemand kann sich anmelden.
|
|
||||||
*
|
*
|
||||||
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per BOOTSTRAP_ADMIN=true
|
* Legt an:
|
||||||
* (siehe docker-compose.coolify.yml und docs/DEPLOY-PROD-CONTABO.md).
|
* 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
|
* Ausführung: im migrate-Job nach `prisma migrate deploy`, gesteuert per
|
||||||
* (kein Passwort-Reset, keine Duplikate). provisionTenant selbst arbeitet mit upserts.
|
* 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:
|
* Erforderliche Umgebungsvariablen:
|
||||||
* BOOTSTRAP_ADMIN_EMAIL E-Mail des ersten Plattform-Admins
|
* BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_NAME
|
||||||
* BOOTSTRAP_ADMIN_PASSWORD Initiales Passwort (nach erstem Login ändern)
|
* BOOTSTRAP_TENANT_NAME, BOOTSTRAP_TENANT_SLUG
|
||||||
* BOOTSTRAP_ADMIN_NAME Anzeigename
|
|
||||||
* BOOTSTRAP_TENANT_NAME Name des ersten Mandanten
|
|
||||||
* BOOTSTRAP_TENANT_SLUG URL-Slug des Mandanten (klein, eindeutig)
|
|
||||||
* Optional:
|
* Optional:
|
||||||
* BOOTSTRAP_TENANT_SHORT Kürzel des Mandanten
|
* BOOTSTRAP_TENANT_SHORT, BOOTSTRAP_TENANT_SECTOR
|
||||||
* BOOTSTRAP_TENANT_SECTOR Branche
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
const prisma = new PrismaClient({
|
||||||
@@ -47,28 +49,29 @@ async function main() {
|
|||||||
const short = process.env.BOOTSTRAP_TENANT_SHORT?.trim() || undefined;
|
const short = process.env.BOOTSTRAP_TENANT_SHORT?.trim() || undefined;
|
||||||
const sector = process.env.BOOTSTRAP_TENANT_SECTOR?.trim() || undefined;
|
const sector = process.env.BOOTSTRAP_TENANT_SECTOR?.trim() || undefined;
|
||||||
|
|
||||||
// Idempotenz: existiert der Plattform-Admin schon, nichts tun (kein PW-Reset).
|
// 1. Mandant + Mandanten-Admin (Zugang /login). Idempotent (upserts).
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenant = await provisionTenant(prisma, {
|
const tenant = await provisionTenant(prisma, {
|
||||||
name: tenantName,
|
name: tenantName,
|
||||||
slug: tenantSlug,
|
slug: tenantSlug,
|
||||||
short,
|
short,
|
||||||
sector,
|
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(
|
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()
|
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 type { Metadata } from "next";
|
||||||
import { Open_Sans, Poppins } from "next/font/google";
|
import localFont from "next/font/local";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
// Self-hosted über next/font (kein Google-CDN-Aufruf zur Laufzeit, DSGVO)
|
// Selbst gehostet über next/font/local (committete woff2 in ./fonts) — kein
|
||||||
const openSans = Open_Sans({
|
// 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",
|
variable: "--font-open-sans",
|
||||||
subsets: ["latin"],
|
display: "swap",
|
||||||
weight: ["300", "400", "600", "700"],
|
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",
|
variable: "--font-poppins",
|
||||||
subsets: ["latin"],
|
display: "swap",
|
||||||
weight: ["300", "500", "600", "700"],
|
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 = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
Reference in New Issue
Block a user