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 }); async function main() { const fruehtracht = await prisma.harvest.upsert({ where: { slug: "fruehtracht-2026" }, update: {}, create: { year: 2026, type: "Frühtracht", slug: "fruehtracht-2026", harvestDate: new Date("2026-05-10"), location: "Altenmünster", description: "Die erste Ernte des Jahres entsteht aus dem Nektar von Obstblüte, Raps und Löwenzahn rund um Altenmünster. Mild, floral und feincremig — der Auftakt jeder Wabenhain-Saison.", images: [], }, }); const sommertracht = await prisma.harvest.upsert({ where: { slug: "sommertracht-2026" }, update: {}, create: { year: 2026, type: "Sommertracht", slug: "sommertracht-2026", harvestDate: new Date("2026-07-18"), location: "Altenmünster", description: "Im Hochsommer sammeln unsere Völker Nektar von Linde, Wiesenblumen und Wald. Das Ergebnis ist kräftiger, aromatischer und charaktervoller als die Frühtracht.", images: [], }, }); const fhBatch = await prisma.batch.upsert({ where: { code: "FH-26-01" }, update: {}, create: { code: "FH-26-01", harvestId: fruehtracht.id, fillDate: new Date("2026-06-02"), bestBeforeDate: new Date("2028-06-02"), glassCount: 200, remainingGlasses: 167, }, }); const stBatch = await prisma.batch.upsert({ where: { code: "ST-26-01" }, update: {}, create: { code: "ST-26-01", harvestId: sommertracht.id, fillDate: new Date("2026-08-05"), bestBeforeDate: new Date("2028-08-05"), glassCount: 150, remainingGlasses: 150, }, }); const fhProduct = await prisma.product.upsert({ where: { slug: "fruehtracht-2026" }, update: { tasteTags: ["mild", "floral", "feincremig"] }, create: { slug: "fruehtracht-2026", name: "Frühtracht 2026", harvestId: fruehtracht.id, batchId: fhBatch.id, description: "Mild, floral und feincremig. Unsere erste Ernte des Jahres aus dem Raum Altenmünster.", tasteTags: ["mild", "floral", "feincremig"], intensityMild: 4, intensityFloral: 4, intensityKraeftig: 2, consistency: "feincremig", weightGrams: 500, priceCents: 990, pricePerKgCents: 1980, images: [], isActive: true, }, }); const stProduct = await prisma.product.upsert({ where: { slug: "sommertracht-2026" }, update: { tasteTags: ["kräftig", "aromatisch", "charaktervoll"] }, create: { slug: "sommertracht-2026", name: "Sommertracht 2026", harvestId: sommertracht.id, batchId: stBatch.id, description: "Kräftiger, aromatischer und charaktervoller Sommerhonig aus Altenmünster.", tasteTags: ["kräftig", "aromatisch", "charaktervoll"], intensityMild: 2, intensityFloral: 2, intensityKraeftig: 5, consistency: "cremig", weightGrams: 500, priceCents: 1090, pricePerKgCents: 2180, images: [], isActive: true, }, }); const candleProduct = await prisma.product.upsert({ where: { slug: "bienenwachskerze-rund" }, update: { tasteTags: ["handgegossen", "reines Bienenwachs", "rußarm"] }, create: { slug: "bienenwachskerze-rund", name: "Bienenwachskerze, rund", category: "KERZEN", description: "Handgegossen aus reinem Bienenwachs unserer eigenen Völker. Brennt gleichmäßig, rußarm und verströmt einen dezenten Honigduft.", tasteTags: ["handgegossen", "reines Bienenwachs", "rußarm"], consistency: "fest", weightGrams: 150, priceCents: 1290, pricePerKgCents: 8600, images: [], isActive: true, }, }); const propolisProduct = await prisma.product.upsert({ where: { slug: "propolis-tinktur" }, update: { tasteTags: ["schonend verarbeitet", "ohne Zusätze"] }, create: { slug: "propolis-tinktur", name: "Propolis-Tinktur", category: "PROPOLIS", description: "Schonend angesetzte Propolis-Tinktur aus dem Harz unserer Bienenstöcke rund um Altenmünster. Ohne synthetische Zusätze.", tasteTags: ["schonend verarbeitet", "ohne Zusätze"], consistency: "flüssig", weightGrams: 30, priceCents: 1490, pricePerKgCents: 49666, images: [], isActive: true, }, }); await prisma.inventory.upsert({ where: { productId: fhProduct.id }, update: { quantity: 167 }, create: { productId: fhProduct.id, quantity: 167 }, }); await prisma.inventory.upsert({ where: { productId: stProduct.id }, update: { quantity: 150 }, create: { productId: stProduct.id, quantity: 150 }, }); await prisma.inventory.upsert({ where: { productId: candleProduct.id }, update: { quantity: 60 }, create: { productId: candleProduct.id, quantity: 60 }, }); await prisma.inventory.upsert({ where: { productId: propolisProduct.id }, update: { quantity: 40 }, create: { productId: propolisProduct.id, quantity: 40 }, }); const existingShippingTiers = await prisma.shippingTier.count(); if (existingShippingTiers === 0) { await prisma.shippingTier.createMany({ data: [ { minQuantity: 0, maxQuantity: 3, priceCents: 700 }, { minQuantity: 4, maxQuantity: 9, priceCents: 850 }, { minQuantity: 10, maxQuantity: 19, priceCents: 1100 }, ], }); console.log("Versandstaffeln angelegt: 0-3=7,00€, 4-9=8,50€, 10-19=11,00€."); } 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." ); } console.log("Seed abgeschlossen: 2 Ernten, 2 Chargen, 4 Produkte (Honig, Kerzen, Propolis)."); } main() .catch((error: unknown) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });