Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import "dotenv/config";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { hashPassword } from "@/server/password";
import { provisionTenant } from "@/server/provision";
/**
* 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.
*
* 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.
*
* 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, BOOTSTRAP_ADMIN_PASSWORD, BOOTSTRAP_ADMIN_NAME
* BOOTSTRAP_TENANT_NAME, BOOTSTRAP_TENANT_SLUG
* Optional:
* BOOTSTRAP_TENANT_SHORT, BOOTSTRAP_TENANT_SECTOR
*/
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;
// 1. Mandant + Mandanten-Admin (Zugang /login). Idempotent (upserts).
const tenant = await provisionTenant(prisma, {
name: tenantName,
slug: tenantSlug,
short,
sector,
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 hashPassword(password), status: "ACTIVE" },
});
console.log(
`✔ 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."
);
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});