- scripts/bootstrap-admin.ts: legt ersten Plattform-Admin + Mandant via provisionTenant(isPlatformAdmin) an; idempotent, per Env gesteuert. Ersetzt in Prod den Demo-Seed. tsx-@/-Auflösung lokal verifiziert. - docker-compose.coolify.yml: migrate-Job führt bei BOOTSTRAP_ADMIN=true das Skript nach der Migration aus; BOOTSTRAP_*-Vars durchgereicht. - .env.prod.example: Prod-Env-Referenz (HTTPS, kein Demo-Seed, Bootstrap-Vars). - docs/DEPLOY-PROD-CONTABO.md: Runbook (VPS-Härtung/Swap, Coolify+Gitea, DNS/TLS app.certvia.de, Deploy, Bootstrap, Backups, Go-Live-Checkliste). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
import "dotenv/config";
|
|
import { PrismaClient } from "@prisma/client";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
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.
|
|
*
|
|
* 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).
|
|
*
|
|
* Idempotent: existiert bereits ein Plattform-Admin mit der E-Mail, passiert nichts
|
|
* (kein Passwort-Reset, keine Duplikate). provisionTenant selbst arbeitet mit upserts.
|
|
*
|
|
* 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)
|
|
* Optional:
|
|
* BOOTSTRAP_TENANT_SHORT Kürzel des Mandanten
|
|
* BOOTSTRAP_TENANT_SECTOR Branche
|
|
*/
|
|
|
|
const prisma = new PrismaClient({
|
|
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
|
|
});
|
|
|
|
function req(name: string): string {
|
|
const v = process.env[name];
|
|
if (!v || v.trim() === "") {
|
|
console.error(`✖ Bootstrap abgebrochen: Umgebungsvariable ${name} fehlt.`);
|
|
process.exit(1);
|
|
}
|
|
return v.trim();
|
|
}
|
|
|
|
async function main() {
|
|
const email = req("BOOTSTRAP_ADMIN_EMAIL").toLowerCase();
|
|
const password = req("BOOTSTRAP_ADMIN_PASSWORD");
|
|
const name = req("BOOTSTRAP_ADMIN_NAME");
|
|
const tenantName = req("BOOTSTRAP_TENANT_NAME");
|
|
const tenantSlug = req("BOOTSTRAP_TENANT_SLUG").toLowerCase();
|
|
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;
|
|
}
|
|
|
|
const tenant = await provisionTenant(prisma, {
|
|
name: tenantName,
|
|
slug: tenantSlug,
|
|
short,
|
|
sector,
|
|
admin: { email, name, password, isPlatformAdmin: true },
|
|
});
|
|
|
|
console.log(
|
|
`✔ Bootstrap fertig: Mandant "${tenant.name}" (${tenantSlug}) + Plattform-Admin ${email} angelegt.`
|
|
);
|
|
console.log(" → Bitte nach dem ersten Login das Passwort ändern.");
|
|
}
|
|
|
|
main()
|
|
.then(() => prisma.$disconnect())
|
|
.catch(async (e) => {
|
|
console.error(e);
|
|
await prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|