import bcrypt from "bcryptjs"; import { PrismaClient } from "../src/generated/prisma/client"; import { PrismaClient as MySqlPrismaClient } from "../src/generated/prisma-mysql/client"; import { createDbAdapter } from "../src/lib/db-adapter"; import { isPasswordBreached } from "../src/lib/password-breach-check"; // Bewusst kein Import aus "@/lib/auth": das Modul zieht next/headers (cookies()) // nach sich, das ausserhalb eines Next.js-Request-Kontexts (hier: Standalone- // Skript via tsx) fehlschlaegt. Der Hash-Rundenwert 12 ist mit auth.ts identisch. const BCRYPT_ROUNDS = 12; const adapter = createDbAdapter(); // Gleiches Prinzip wie lib/prisma.ts: zwei generierte Clients im Image, // Auswahl zur Laufzeit ueber DATABASE_PROVIDER. const prisma: PrismaClient = (process.env.DATABASE_PROVIDER ?? "postgresql").trim() === "mysql" ? (new MySqlPrismaClient({ adapter }) as unknown as PrismaClient) : new PrismaClient({ adapter }); /** * Laeuft automatisch bei jedem Container-Start (siehe Dockerfile-CMD in der * runner-Stage) — legt/erneuert NUR das Admin-Konto, keine Beispieldaten. * Ein produktiv gebautes Image soll nie automatisch Fake-Produkte in einer * echten Datenbank anlegen. Beispieldaten fuer lokales Testen/Screenshots: * "pnpm db:seed:demo" (prisma/seed-demo.ts), manuell und getrennt. */ async function main() { const adminEmail = process.env.ADMIN_EMAIL?.trim().toLowerCase(); const adminPassword = process.env.ADMIN_PASSWORD; if (adminEmail && adminPassword) { // Dieselben Mindestanforderungen wie bei der normalen Kundenregistrierung // (registration.ts) — ein Admin-Konto hat Zugriff auf Bestellungen, // Rueckerstattungen und Kundendaten und verdient keine schwaecheren // Passwort-Regeln nur weil es per Seed statt per Formular angelegt wird. if (adminPassword.length < 8) { throw new Error("ADMIN_PASSWORD muss mindestens 8 Zeichen lang sein."); } if (await isPasswordBreached(adminPassword)) { throw new Error( "ADMIN_PASSWORD wurde in bekannten Datenlecks gefunden (Have I Been Pwned). Bitte ein anderes Passwort waehlen." ); } const existingAtEmail = await prisma.customer.findUnique({ where: { email: adminEmail } }); if (!existingAtEmail) { await prisma.customer.create({ data: { email: adminEmail, passwordHash: await bcrypt.hash(adminPassword, BCRYPT_ROUNDS), firstName: "Admin", lastName: "Wabenhain", role: "ADMIN", }, }); console.log(`Admin-Konto angelegt: ${adminEmail} (Passwort aus ADMIN_PASSWORD).`); } else if (existingAtEmail.role === "ADMIN") { // Bereits Admin: Passwort-Rotation ueber ein geaendertes ADMIN_PASSWORD // weiterhin unterstuetzen (dokumentierter Weg, ein Admin-Passwort zu // erneuern) — unproblematisch, da das Konto schon Admin ist. await prisma.customer.update({ where: { id: existingAtEmail.id }, data: { passwordHash: await bcrypt.hash(adminPassword, BCRYPT_ROUNDS) }, }); console.log(`Admin-Passwort synchronisiert: ${adminEmail}.`); } else { // Sicherheitsrelevant: seed befoerdert NIE automatisch ein bestehendes // Nicht-Admin-Konto zu ADMIN, nur weil dessen E-Mail mit ADMIN_EMAIL // uebereinstimmt — sonst koennte sich jemand die Admin-Mail vor dem // ersten Deploy-Seed-Lauf selbst registrieren und beim naechsten // "pnpm prisma db seed" automatisch Admin werden, mit selbstgewaehltem // Passwort. Promotion eines bestehenden Kontos daher nur manuell ueber // die Datenbank, siehe README "Admin-Zugang". console.warn( `WARNUNG: ${adminEmail} existiert bereits als Kundenkonto (nicht Admin) und wird NICHT automatisch befoerdert. ` + 'Siehe README, Abschnitt "Admin-Zugang", fuer die manuelle Befoerderung.' ); } } else { console.log( "Kein Admin-Konto angelegt: ADMIN_EMAIL/ADMIN_PASSWORD sind nicht gesetzt. " + "Siehe README, wie ein bestehendes Konto nachtraeglich zu ADMIN befoerdert wird." ); } await seedSponsorshipDefaults(); } /** * Startangebote der Bienenpatenschaft und Treuestufen — nur wenn es noch * keine gibt, und die Angebote INAKTIV: dieser Seed laeuft bei jedem * Containerstart auch in Produktion, und Preise, Honig und Rechtstexte muss * der Inhaber vor der Veroeffentlichung pruefen (Spec, "Geschaeftliche * Konfiguration"). Erst das Aktivieren im Admin macht /patenschaft sichtbar. */ async function seedSponsorshipDefaults(): Promise { if ((await prisma.sponsorshipOffer.count()) === 0) { const packages = [ { slug: "bienenfreund", name: "Bienenfreund", nameEn: "Bee Friend", priceCents: 1900, isPhysical: false, honeyJars: 0, medalTier: "BRONZE" as const, description: "Symbolische Namensgebung mit individuell gestalteter Urkunde als PDF.", descriptionEn: "Symbolic naming with an individually designed certificate as a PDF.", features: ["Symbolische Namensgebung", "Individuell gestaltete PDF-Urkunde", "Medaille und 3D-Ansicht im Kundenkonto", "Sicherer Download"], featuresEn: ["Symbolic naming", "Individually designed PDF certificate", "Medal and 3D view in your account", "Secure download"], }, { slug: "honigfreund", name: "Honigfreund", nameEn: "Honey Friend", priceCents: 3900, isPhysical: true, honeyJars: 1, medalTier: "SILBER" as const, description: "Digitale Leistungen plus gedruckte Urkunde, ein Glas Wabenhain-Honig, Grußkarte und Geschenkverpackung.", descriptionEn: "Digital items plus a printed certificate, a jar of Wabenhain honey, a greeting card and gift wrapping.", features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "1 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"], featuresEn: ["All digital items", "High-quality printed certificate", "1 × 500 g Wabenhain honey", "Greeting card and gift wrapping"], }, { slug: "wabenfreund", name: "Wabenfreund", nameEn: "Comb Friend", priceCents: 5900, isPhysical: true, honeyJars: 3, medalTier: "GOLD" as const, description: "Digitale Leistungen plus gedruckte Urkunde, drei Gläser Wabenhain-Honig, Grußkarte und Geschenkverpackung.", descriptionEn: "Digital items plus a printed certificate, three jars of Wabenhain honey, a greeting card and gift wrapping.", features: ["Alle digitalen Leistungen", "Hochwertig gedruckte Urkunde", "3 × 500 g Wabenhain-Honig", "Grußkarte und Geschenkverpackung"], featuresEn: ["All digital items", "High-quality printed certificate", "3 × 500 g Wabenhain honey", "Greeting card and gift wrapping"], }, ]; for (const [index, pkg] of packages.entries()) { // Honiganteil als Vorschlag (7 € je Glas) — der Inhaber prueft ihn mit // der Steuerberatung, bevor er das Angebot aktiviert. const honeyShareCentsPerJar = pkg.honeyJars > 0 ? 700 : 0; const product = await prisma.product.create({ data: { slug: `patenschaft-${pkg.slug}`, name: `Bienenpatenschaft ${pkg.name}`, category: "PATENSCHAFT", description: pkg.description, weightGrams: 0, priceCents: pkg.priceCents - pkg.honeyJars * honeyShareCentsPerJar, pricePerKgCents: 0, isActive: true, }, }); await prisma.sponsorshipOffer.create({ data: { slug: pkg.slug, kind: "EINMALIG", name: pkg.name, nameEn: pkg.nameEn, description: pkg.description, descriptionEn: pkg.descriptionEn, features: pkg.features, featuresEn: pkg.featuresEn, priceCents: pkg.priceCents, isActive: false, sortOrder: index, isPhysical: pkg.isPhysical, productId: product.id, honeyJars: pkg.honeyJars, honeyShareCentsPerJar, medalTier: pkg.medalTier, }, }); } console.log("Bienenpatenschaft: drei Startangebote angelegt (inaktiv, bitte im Admin pruefen und aktivieren)."); } if ((await prisma.loyaltyLevel.count()) === 0) { const regular = [ { threshold: 2, name: "Blütenbesuch", nameEn: "Blossom Visitor" }, { threshold: 5, name: "Wabenbau", nameEn: "Comb Builder" }, { threshold: 10, name: "Honigschatz", nameEn: "Honey Treasure" }, { threshold: 25, name: "Goldwabe", nameEn: "Golden Comb" }, ]; const subscription = Array.from({ length: 24 }, (_, i) => ({ threshold: i + 1, name: `Stufe ${i + 1}`, nameEn: `Level ${i + 1}`, })); await prisma.loyaltyLevel.createMany({ data: [ ...regular.map((level) => ({ kind: "STAMMKUNDE" as const, ...level })), ...subscription.map((level) => ({ kind: "ABO" as const, ...level })), { kind: "ABO" as const, threshold: null, name: "VIP", nameEn: "VIP" }, ], }); console.log("Treuestufen angelegt (Stammkunde 2/5/10/25, Abo 1–24 und VIP)."); } } main() .catch((error: unknown) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });