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:
1 parent
f5b1593e46
commit
b5d3df6d7c
1 file changed
+229
@@ -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 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user