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
This commit is contained in:
Z User committed 2026-10-28 21:09:56 +00:00
1 parent f5b1593e46
commit b5d3df6d7c
1 file changed
+229
+229
View File
@@ -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<string, number> = {}
// --- 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<string, string> = {
'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<string, string> = {}
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 })
}
}