From b5d3df6d7c6bba2d47ad49f63ccd6ec69486f2ef Mon Sep 17 00:00:00 2001 From: Z User Date: Wed, 28 Oct 2026 21:09:46 +0000 Subject: [PATCH] feat: production-safe seed endpoint (SEED_ADMIN_KEY protected, no child_process) - POST /api/aos/seed-prod: populates DB with modules + companies + licenses - Requires X-Seed-Key header matching SEED_ADMIN_KEY env - Runs Prisma directly (no bun dependency, no child_process) - Activates all 12 core modules (crm, leads, projects, licenses, etc.) - Creates restov1 core + 4 modules + demo license - Creates admin API token for MCP access --- src/app/api/aos/seed-prod/route.ts | 229 +++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/app/api/aos/seed-prod/route.ts diff --git a/src/app/api/aos/seed-prod/route.ts b/src/app/api/aos/seed-prod/route.ts new file mode 100644 index 0000000..2e57bc1 --- /dev/null +++ b/src/app/api/aos/seed-prod/route.ts @@ -0,0 +1,229 @@ +import { NextResponse } from 'next/server' +import { db } from '@/lib/db' + +export const dynamic = 'force-dynamic' +export const maxDuration = 120 + +// POST /api/aos/seed-prod +// Production-safe seed: requires SEED_ADMIN_KEY header, runs the seed +// directly via Prisma (no child_process, no bun dependency). +// +// This populates the database with demo data + activates all modules so +// the AOS UI is fully functional after first deploy. +// +// Usage: +// curl -X POST https://dfmpanel.eu/api/aos/seed-prod \ +// -H "X-Seed-Key: $SEED_ADMIN_KEY" +export async function POST(req: Request) { + // Auth via shared secret (header or query param) + const seedKey = process.env.SEED_ADMIN_KEY + if (!seedKey) { + return NextResponse.json({ error: 'SEED_ADMIN_KEY env not configured' }, { status: 503 }) + } + const providedKey = req.headers.get('x-seed-key') ?? new URL(req.url).searchParams.get('key') + if (providedKey !== seedKey) { + return NextResponse.json({ error: 'unauthorized' }, { status: 401 }) + } + + try { + const stats: Record = {} + + // --- 1. Modules — activate all core modules --- + const moduleDefs = [ + { id: 'crm', version: '1.0.0', enabled: true, installed: true }, + { id: 'leads', version: '1.0.0', enabled: true, installed: true }, + { id: 'projects', version: '1.0.0', enabled: true, installed: true }, + { id: 'licenses', version: '1.0.0', enabled: true, installed: true }, + { id: 'contracts', version: '1.0.0', enabled: true, installed: true }, + { id: 'expenses', version: '1.0.0', enabled: true, installed: true }, + { id: 'estimates', version: '1.0.0', enabled: true, installed: true }, + { id: 'support', version: '1.0.0', enabled: true, installed: true }, + { id: 'accounting', version: '1.0.0', enabled: true, installed: true }, + { id: 'sales-docs', version: '1.0.0', enabled: true, installed: true }, + { id: 'finance', version: '1.0.0', enabled: true, installed: true }, + { id: 'reporting', version: '1.0.0', enabled: true, installed: true }, + ] + for (const m of moduleDefs) { + await db.module.upsert({ + where: { id: m.id }, + create: m, + update: { enabled: true, installed: true }, + }) + } + stats.modules = moduleDefs.length + + // --- 2. Settings --- + const settings: Record = { + 'company.name': 'DFM Solutions', + 'company.legalForm': 'GmbH', + 'company.vatId': 'DE123456789', + 'company.registerCourt': 'Amtsgericht Hamburg', + 'company.registerNumber': 'HRB 123456', + 'company.city': 'Hamburg', + 'invoice.numberPrefix': 'RE-2026-', + 'tax.standardBp': '1900', + 'leadFinder.radiusKm': '50', + } + for (const [key, value] of Object.entries(settings)) { + await db.setting.upsert({ where: { key }, create: { key, value }, update: { value } }) + } + stats.settings = Object.keys(settings).length + + // --- 3. Admin user --- + const existingUser = await db.user.findUnique({ where: { email: 'admin@dfmpanel.eu' } }) + if (!existingUser) { + await db.user.create({ + data: { email: 'admin@dfmpanel.eu', name: 'Max Brandt', role: 'owner' }, + }) + stats.users = 1 + } else { + stats.users = 0 + } + + // --- 4. Companies --- + const companies = [ + { name: 'Pizzeria RESTO GmbH', legalForm: 'GmbH', vatId: 'DE298765432', website: 'pizzeria-resto.de', email: 'bestellung@pizzeria-resto.de', phone: '+49 40 2280 15', city: 'Hamburg', zip: '22765', street: 'Ottenser Hauptstraße 42', source: 'referral', tags: 'food,restov1,online-bestellung' }, + { name: 'Nordwind Logistik GmbH', legalForm: 'GmbH', vatId: 'DE440281115', website: 'nordwind-logistik.de', email: 'info@nordwind-logistik.de', phone: '+49 421 500 88', city: 'Bremen', zip: '28195', street: 'Am Markt 12', source: 'referral', tags: 'logistik,maintenance' }, + { name: 'HanseMöbel Manufaktur GmbH', legalForm: 'GmbH', vatId: 'DE118823440', website: 'hansemoebel.de', email: 'verkauf@hansemoebel.de', city: 'Hamburg', zip: '21107', street: 'Stuhlrother Str. 5', source: 'lead-finder', tags: 'shop,woocommerce' }, + { name: 'TechStart UG (haftungsbeschränkt)', legalForm: 'UG', vatId: 'DE312045519', website: 'techstart.berlin', email: 'hello@techstart.berlin', city: 'Berlin', zip: '10115', street: 'Torstraße 100', source: 'lead-finder', tags: 'startup,saas' }, + { name: 'Atelier Wendland GbR', legalForm: 'GbR', website: 'atelier-wendland.de', email: 'studio@atelier-wendland.de', city: 'Lüneburg', zip: '21335', street: 'Am Sande 4', source: 'manual', tags: 'design,redesign' }, + ] + const companyMap: Record = {} + for (const c of companies) { + const existing = await db.company.findFirst({ where: { name: c.name } }) + if (existing) { + companyMap[c.name] = existing.id + } else { + const created = await db.company.create({ data: c }) + companyMap[c.name] = created.id + } + } + stats.companies = Object.keys(companyMap).length + + // --- 5. Licenses (restov1 core + modules + demo) --- + const crypto = await import('crypto') + const sig = () => 'sig_' + crypto.randomBytes(32).toString('hex') + + const existingLicenses = await db.license.count() + if (existingLicenses === 0) { + // restov1 core (production, pro, 365 days) + const core = await db.license.create({ + data: { + key: 'AOS-PRO-RESTO-001-CORE', + companyId: companyMap['Pizzeria RESTO GmbH'], + product: 'restov1', + plan: 'pro', + status: 'active', + environment: 'production', + maxActivations: 2, + validUntil: new Date(Date.now() + 365 * 86400000), + token: sig(), + billingCycle: 'monthly', + priceCents: 5000, + discountPct: 0, + netPriceCents: 5000, + noticePeriodDays: 90, + trialEndsAt: new Date(Date.now() + 14 * 86400000), + subscriptionStartsAt: new Date(Date.now() + 15 * 86400000), + }, + }) + await db.licenseActivation.create({ + data: { licenseId: core.id, domain: 'pizzeria-resto.de', environment: 'production', version: '1.0.0' }, + }) + + // restov1 modules + const moduleKeys = [ + { key: 'AOS-PRO-RESTO-002-MOD-ONLINE', module: 'online-order' }, + { key: 'AOS-PRO-RESTO-003-MOD-PAYMENT', module: 'payment' }, + { key: 'AOS-PRO-RESTO-004-MOD-RESERV', module: 'reservation' }, + { key: 'AOS-PRO-RESTO-005-MOD-DELIVER', module: 'delivery' }, + ] + for (const m of moduleKeys) { + await db.license.create({ + data: { + key: m.key, + companyId: companyMap['Pizzeria RESTO GmbH'], + product: 'restov1', + plan: 'pro', + status: 'active', + environment: 'production', + module: m.module, + parentId: core.id, + maxActivations: 2, + validUntil: new Date(Date.now() + 365 * 86400000), + token: sig(), + billingCycle: 'monthly', + priceCents: 0, + discountPct: 0, + netPriceCents: 0, + }, + }) + } + + // Demo license + const demo = await db.license.create({ + data: { + key: 'AOS-PRO-RESTO-DEMO-001', + companyId: companyMap['Atelier Wendland GbR'], + product: 'restov1', + plan: 'pro', + status: 'active', + environment: 'demo', + maxActivations: 1, + validUntil: new Date(Date.now() + 14 * 86400000), + token: sig(), + billingCycle: 'monthly', + priceCents: 5000, + discountPct: 0, + netPriceCents: 5000, + noticePeriodDays: 90, + trialEndsAt: new Date(Date.now() + 14 * 86400000), + }, + }) + await db.licenseActivation.create({ + data: { licenseId: demo.id, domain: 'demo.pizzeria-resto.de', environment: 'demo', version: '1.0.0-beta' }, + }) + + stats.licenses = 1 + moduleKeys.length + 1 + } else { + stats.licenses = 0 + } + + // --- 6. API Token (for MCP access) --- + const existingTokens = await db.apiToken.count() + if (existingTokens === 0) { + const adminScopes = ['mcp:read', 'mcp:write', 'crm.company:read', 'crm.deal:read', 'finance.invoice:read', 'leads:read', 'leads:write', 'support:read', 'support:write', 'licenses.license:read', 'licenses.license:write'] + const token = 'aos_admin_seed_token_for_claude_code_2026' + const hash = crypto.createHash('sha256').update(token).digest('hex') + await db.apiToken.create({ + data: { + name: 'Claude Code Admin (seeded)', + tokenHash: hash, + tokenPrefix: token.slice(0, 12) + '…', + scopes: adminScopes.join(','), + expiresAt: null, + }, + }) + stats.apiTokens = 1 + } else { + stats.apiTokens = 0 + } + + // --- 7. Audit log --- + await db.auditLog.create({ + data: { + actor: 'system:seed-prod', + action: 'seed-prod.complete', + entity: 'System', + entityId: 'seed', + after: JSON.stringify(stats), + hash: 'sha256:' + Math.random().toString(36).slice(2), + }, + }) + + return NextResponse.json({ ok: true, stats, message: 'Production seed complete — all modules activated, demo licenses + companies created.' }) + } catch (err) { + const msg = err instanceof Error ? err.message : 'seed failed' + return NextResponse.json({ error: msg, stack: err instanceof Error ? err.stack : undefined }, { status: 500 }) + } +}