Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m4s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 57s
CI / test-python-voice (push) Successful in 42s
CI / test-bqas (push) Successful in 42s
D1: Remove /api/admin/fp-patch from PUBLIC_PATHS — it was returning live financial data (fp_liquiditaet rows) to any unauthenticated caller; middleware admin gate now applies as it does for all /api/admin/* paths. D2: Add PITCH_ADMIN_SECRET bearer guard to POST /api/financial-model (create scenario) and PUT /api/financial-model/assumptions (update assumptions) — any authenticated investor could previously create/modify global financial model data. D3: Add PITCH_ADMIN_SECRET bearer guard to POST /api/finanzplan/compute — any investor could trigger a full DB recomputation across all fp_* tables. Also replace String(error) in error response with a static message. D4: GET /api/finanzplan/[sheetName] now ignores ?scenarioId= for non-admin callers; investors always receive the default scenario only. Previously any investor could enumerate UUIDs and read any scenario's financials including other investors' plans. D9: Remove `name` from the non-admin /api/finanzplan response — scenario names like "Wandeldarlehen v2" reveal internal versioning to investors. D10: Remove hardcoded postgres://breakpilot:breakpilot123@localhost fallback from lib/db.ts — missing DATABASE_URL now fails loudly instead of silently using stale credentials that are committed to the repository. D6: Fix all 4 TypeScript errors that were masked by ignoreBuildErrors:true; bump tsconfig target to ES2018 (regex s flag in ChatFAB), type lang as 'de'|'en' in chat route, add 'as string' assertion in adapter.ts. Remove ignoreBuildErrors:true from next.config.js so future type errors fail the build rather than being silently shipped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import pool from '@/lib/db'
|
|
import { getSessionFromCookie, validateAdminSecret } from '@/lib/auth'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
function assembleScenarios(scenarioRows: Record<string, unknown>[], assumptionRows: Record<string, unknown>[]) {
|
|
return scenarioRows.map(s => ({
|
|
...s,
|
|
assumptions: assumptionRows
|
|
.filter((a: Record<string, unknown>) => a.scenario_id === (s as Record<string, unknown>).id)
|
|
.map((a: Record<string, unknown>) => ({
|
|
...a,
|
|
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
|
|
})),
|
|
}))
|
|
}
|
|
|
|
// GET: Load all scenarios with their assumptions (version-aware)
|
|
export async function GET() {
|
|
try {
|
|
// Check if investor has an assigned version with FM data
|
|
const session = await getSessionFromCookie()
|
|
let versionId: string | null = null
|
|
|
|
if (session) {
|
|
const inv = await pool.query(
|
|
`SELECT assigned_version_id FROM pitch_investors WHERE id = $1`,
|
|
[session.sub],
|
|
)
|
|
versionId = inv.rows[0]?.assigned_version_id || null
|
|
}
|
|
|
|
if (versionId) {
|
|
const [scenarioData, assumptionData] = await Promise.all([
|
|
pool.query(`SELECT data FROM pitch_version_data WHERE version_id = $1 AND table_name = 'fm_scenarios'`, [versionId]),
|
|
pool.query(`SELECT data FROM pitch_version_data WHERE version_id = $1 AND table_name = 'fm_assumptions'`, [versionId]),
|
|
])
|
|
|
|
if (scenarioData.rows.length > 0) {
|
|
const scenarios = typeof scenarioData.rows[0].data === 'string'
|
|
? JSON.parse(scenarioData.rows[0].data) : scenarioData.rows[0].data
|
|
const assumptions = assumptionData.rows.length > 0
|
|
? (typeof assumptionData.rows[0].data === 'string'
|
|
? JSON.parse(assumptionData.rows[0].data) : assumptionData.rows[0].data)
|
|
: []
|
|
return NextResponse.json(assembleScenarios(scenarios, assumptions))
|
|
}
|
|
}
|
|
|
|
// Fallback: base tables
|
|
const client = await pool.connect()
|
|
try {
|
|
const scenarios = await client.query(
|
|
'SELECT * FROM pitch_fm_scenarios ORDER BY is_default DESC, name'
|
|
)
|
|
const assumptions = await client.query(
|
|
'SELECT * FROM pitch_fm_assumptions ORDER BY sort_order'
|
|
)
|
|
return NextResponse.json(assembleScenarios(scenarios.rows, assumptions.rows))
|
|
} finally {
|
|
client.release()
|
|
}
|
|
} catch (error) {
|
|
console.error('Financial model load error:', error)
|
|
return NextResponse.json({ error: 'Failed to load scenarios' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
// POST: Create a new scenario — admin only
|
|
export async function POST(request: NextRequest) {
|
|
if (!validateAdminSecret(request)) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
try {
|
|
const body = await request.json()
|
|
const { name, description, color, copyFrom } = body
|
|
|
|
if (!name) {
|
|
return NextResponse.json({ error: 'Name is required' }, { status: 400 })
|
|
}
|
|
|
|
const client = await pool.connect()
|
|
try {
|
|
const scenario = await client.query(
|
|
'INSERT INTO pitch_fm_scenarios (name, description, color) VALUES ($1, $2, $3) RETURNING *',
|
|
[name, description || '', color || '#6366f1']
|
|
)
|
|
|
|
// If copyFrom is set, copy assumptions from another scenario
|
|
if (copyFrom) {
|
|
await client.query(`
|
|
INSERT INTO pitch_fm_assumptions (scenario_id, key, label_de, label_en, value, value_type, unit, min_value, max_value, step_size, category, sort_order)
|
|
SELECT $1, key, label_de, label_en, value, value_type, unit, min_value, max_value, step_size, category, sort_order
|
|
FROM pitch_fm_assumptions WHERE scenario_id = $2
|
|
`, [scenario.rows[0].id, copyFrom])
|
|
}
|
|
|
|
return NextResponse.json(scenario.rows[0])
|
|
} finally {
|
|
client.release()
|
|
}
|
|
} catch (error) {
|
|
console.error('Create scenario error:', error)
|
|
return NextResponse.json({ error: 'Failed to create scenario' }, { status: 500 })
|
|
}
|
|
}
|