import 'dotenv/config'; import * as bcrypt from 'bcryptjs'; import * as crypto from 'node:crypto'; import { PrismaPg } from '@prisma/adapter-pg'; import { Prisma, PrismaClient } from '../generated/prisma'; const BCRYPT_SALT_ROUNDS = 12; const DEFAULT_ADMIN_EMAIL = 'admin@apicore.local'; // No fixed fallback password: a fixed literal shipped in source would be a // well-known, guessable credential for any deployment that forgets to set // SEED_ADMIN_PASSWORD. A random one is generated instead and printed once // below, same as an operator-supplied one would be. function generateRandomPassword(): string { return crypto.randomBytes(18).toString('base64url'); } // Only printed the moment the account is actually created (first deploy on // an empty user table), never on later restarts: by then the real admin may // have changed the password via /profile, so repeating it would just be // stale and would leak credentials into logs indefinitely for no reason. // An operator-supplied password is never printed: the operator already has // it, and the log would only be one more place it leaks to. function printLoginBanner( email: string, password: string | null, log: (line: string) => void, ): void { const url = process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000'; const line = '='.repeat(60); log(line); log('Seed: Admin-Zugang eingerichtet.'); log(` URL: ${url}`); log(` Benutzer: ${email}`); log( password === null ? ' Passwort: wie in SEED_ADMIN_PASSWORD gesetzt' : ` Passwort: ${password}`, ); log(line); } // Duplicated from src/task-templates/recurrence.ts rather than imported: // tsconfig.seed.json scopes this build to rootDir "./prisma" only (its own // isolated compile, separate from the main Nest build), so a cross-rootDir // import would fail to compile. Only the YEARLY case is needed here. function nextYearlyOccurrenceUtc( month: number, dayOfMonth: number, from: Date, ): Date { const year = from.getUTCFullYear(); const clampedDay = (y: number): number => Math.min(dayOfMonth, new Date(Date.UTC(y, month, 0)).getUTCDate()); const today = new Date( Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate()), ); const candidate = new Date(Date.UTC(year, month - 1, clampedDay(year))); if (candidate.getTime() >= today.getTime()) { return candidate; } return new Date(Date.UTC(year + 1, month - 1, clampedDay(year + 1))); } interface DefaultTaskTemplate { title: string; description: string; priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT'; month: number; dayOfMonth: number; } // The standard German-beekeeping annual cycle, one flagship task per month - // gives a new deployment a ready-made starter calendar instead of a // completely empty task-template list. Admins can edit/deactivate any of // these afterward; this only ever runs once (see the count() guard below). const DEFAULT_TASK_TEMPLATES: DefaultTaskTemplate[] = [ { title: 'Wintersitz kontrollieren', description: 'Ruhige Kontrolle von außen: Flugloch frei, Gewicht per Anheben schätzen, auf Mäuse-/Spechtschäden achten. Beute möglichst nicht öffnen.', priority: 'NORMAL', month: 1, dayOfMonth: 15, }, { title: 'Gemülldiagnose', description: 'Windel auswerten: natürlicher Varroa-Totenfall, Bienensitz und Lage der Wintertraube anhand des Gemülls einschätzen.', priority: 'NORMAL', month: 2, dayOfMonth: 15, }, { title: 'Frühjahrsdurchsicht', description: 'Erste Innendurchsicht bei ca. 15°C und ruhigem Wetter: Brutnest, Futtervorrat, Volksstärke und Weiselrichtigkeit prüfen.', priority: 'HIGH', month: 3, dayOfMonth: 15, }, { title: 'Volk erweitern', description: 'Zargen/Rähmchen nach Volksstärke erweitern, Baurahmen einhängen, Platz für die Frühtracht schaffen.', priority: 'NORMAL', month: 4, dayOfMonth: 1, }, { title: 'Schwarmkontrolle beginnen', description: 'Wöchentlich auf Weiselzellen kontrollieren, bis zum Ende der Schwarmzeit fortführen. Maßnahmen bei Bedarf: Ableger bilden, Zellen brechen.', priority: 'HIGH', month: 5, dayOfMonth: 1, }, { title: 'Honigernte vorbereiten', description: 'Reifegrad der Frühtracht prüfen (Spritzprobe), Schleuderplan und Gerätereinigung vorbereiten.', priority: 'NORMAL', month: 6, dayOfMonth: 15, }, { title: 'Varroa-Kontrolle nach der Ernte', description: 'Milbenbefall messen (z. B. Puderzuckermethode oder natürlicher Totenfall) und Behandlung planen, sobald die Honigräume abgeräumt sind.', priority: 'HIGH', month: 7, dayOfMonth: 15, }, { title: 'Einfütterung beginnen', description: 'Wintervorrat auffüttern (Zielgewicht je nach Beutensystem), Futteraufnahme nach ein bis zwei Wochen kontrollieren.', priority: 'HIGH', month: 8, dayOfMonth: 1, }, { title: 'Varroa-Restentmilbung / Winterfestigkeit prüfen', description: 'Zweite Varroabehandlung durchführen, Volksstärke und Weiselrichtigkeit für den Winter beurteilen.', priority: 'HIGH', month: 9, dayOfMonth: 15, }, { title: 'Fütterung abschließen, Mäusegitter anbringen', description: 'Letzte Futtergabe, Fluglochkeile bzw. Mäusegitter montieren, Bienenstand winterfest machen.', priority: 'NORMAL', month: 10, dayOfMonth: 1, }, { title: 'Winterbehandlung (Oxalsäure)', description: 'Brutfreiheit abwarten und Träufel- oder Sprühbehandlung gegen Varroa durchführen.', priority: 'HIGH', month: 11, dayOfMonth: 15, }, { title: 'Jahresrückblick und Gerätewartung', description: 'Saison auswerten, Zargen und Rähmchen reinigen/reparieren, Material für die kommende Saison planen.', priority: 'LOW', month: 12, dayOfMonth: 15, }, ]; async function seedDefaultTaskTemplates(prisma: PrismaClient): Promise { const existingCount = await prisma.taskTemplate.count(); if (existingCount > 0) { return; } const now = new Date(); await prisma.taskTemplate.createMany({ data: DEFAULT_TASK_TEMPLATES.map((template) => ({ title: template.title, description: template.description, priority: template.priority, recurrenceType: 'YEARLY', recurrenceMonth: template.month, recurrenceDayOfMonth: template.dayOfMonth, nextDueDate: nextYearlyOccurrenceUtc( template.month, template.dayOfMonth, now, ), })), }); console.log( `Seed: ${DEFAULT_TASK_TEMPLATES.length} Standard-Aufgabenvorlagen angelegt.`, ); } type AdminSeedClient = Pick; /** * Creates the first administrator, but only while the user table is still * empty. Checking SEED_ADMIN_EMAIL instead would add a second ADMIN (and log * its password) on every start after the real admin changed their address. */ export async function seedAdmin( prisma: AdminSeedClient, env: NodeJS.ProcessEnv, log: (line: string) => void, ): Promise<'created' | 'skipped'> { if ((await prisma.user.count()) > 0) { log('Seed: Es gibt bereits Benutzer, kein Admin angelegt.'); return 'skipped'; } const email = env.SEED_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL; const suppliedPassword = env.SEED_ADMIN_PASSWORD; const password = suppliedPassword ?? generateRandomPassword(); const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); const adminRole = await prisma.role.findUniqueOrThrow({ where: { key: 'ADMIN' }, }); try { await prisma.user.create({ data: { email, passwordHash, name: 'Administrator', roleId: adminRole.id, }, }); } catch (error: unknown) { // P2002 = unique constraint violation on email: another replica's // startup won this race between the count above and this create() // call. The user now exists either way, so this is a no-op, not a // failure - container startup must not crash on it. if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002' ) { log(`Seed: admin user ${email} was created concurrently, skipping.`); return 'skipped'; } throw error; } printLoginBanner( email, suppliedPassword === undefined ? password : null, log, ); return 'created'; } async function main(): Promise { const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), }); await seedDefaultTaskTemplates(prisma); await seedAdmin(prisma, process.env, (line) => console.log(line)); await prisma.$disconnect(); } // Only when run as `node dist/seed.js`, not when a test imports seedAdmin. if (require.main === module) { main().catch((error: unknown) => { console.error(error); process.exit(1); }); }