sharang created pull request Benjamin_Boenisch/breakpilot-core#3

sharang
Tue, 07 Apr 2026 09:28:18 +0000

3#feat(pitch-deck): admin UI for investor + financial-model management#

Summary

Adds a full admin dashboard at /pitch-admin so the founders can invite investors, see who's logged in and what they viewed, edit investor profiles, resend magic links, revoke access, manage admin accounts, and edit financial-model defaults — all with full audit attribution.

Auth model

New pitch_admins table with bcrypt passwords (cost 12) and pitch_admin_sessions with single-active-session enforcement

Separate pitch_admin_session cookie + JWT with pitch-admin audience claim (so an investor JWT can never be mistaken for an admin one)

2h JWT TTL, 12h DB session TTL

lib/admin-auth.ts mirrors lib/auth.ts (bcryptjs, jose, single-session, getAdminFromCookie)

requireAdmin(request) API guard returns admin row OR 401 NextResponse

Backward compat: existing Authorization: Bearer $PITCH_ADMIN_SECRET curl access still works on /api/admin/* (logged as actor='cli')

Audit attribution

pitch_audit_logs extended with admin_id (actor) and target_investor_id (target)

logAudit accepts trailing adminId + targetInvestorId params

logAdminAudit wrapper for admin-initiated events

Every state-changing admin action emits one audit row attributing the action to the calling admin and the affected target

New audit actions: admin_login_success, admin_login_failed, admin_logout, investor_invited, magic_link_resent, investor_edited, investor_revoked (existing, now attributed), admin_created, admin_edited, admin_deactivated, scenario_edited, assumption_edited

API surface

New (/api/admin-auth/*): login, logout, me

New (/api/admin/*): dashboard, investors/[id] GET+PATCH, investors/[id]/resend, admins GET+POST, admins/[id] PATCH, fm/scenarios GET, fm/scenarios/[id] PATCH, fm/assumptions/[id] PATCH

Migrated to requireAdmin: invite, investors (list), revoke, audit-logs (now supports actor_type, admin_id, target_investor_id, since/until filters)

Frontend

/pitch-admin/login — email + password form (separate layout, bypasses the authed shell)

/pitch-admin/(authed)/... — route group with shared AdminShell (sidebar + topbar, dark theme matching the pitch deck)

Dashboard: 4 KPI cards (total / active 7d / pending invites / slides viewed), recent logins, recent activity

Investors: searchable + status-filtered table with inline resend/revoke; click row → detail page

Investor detail: inline edit name/company, login history, snapshot count, full per-investor audit timeline

Audit log: actor_type + action filters, paginated 50/page

Financial model: list scenarios → edit assumptions categorized, inline edit with before/after diff

Admins: list, add, deactivate, reset password (revokes sessions on password change)

Bootstrap

pitch-deck/scripts/create-admin.ts (npm run admin:create -- --email=... --name=... --password=...). Idempotent: existing admin gets password updated. Also reads from PITCH_ADMIN_BOOTSTRAP_* env vars for non-interactive use.

Files

27 new files (migration, lib, scripts, 14 API routes, 9 admin pages, 3 components)

9 modified (middleware, lib/auth, 4 existing admin endpoints, package.json/lock, docker-compose.coolify.yml)

36 files changed, 3424 insertions, 66 deletions

New deps: bcryptjs + @types/bcryptjs + tsx (dev)

Test plan

Run migration 002_admin_users.sql against PostgreSQL

Bootstrap admin: npm run admin:create -- --email=ben@breakpilot.ai --name='Benjamin' --password='...'

Visit /pitch-admin/login, enter credentials → redirected to dashboard with KPIs

Invite investor from /pitch-admin/investors/new → magic link email arrives

Check /pitch-admin/audit → row investor_invited with admin_id + target_investor_id populated

Investor clicks magic link → logs in → returns to /pitch-admin, "Active 7d" goes up

Edit investor name → audit row investor_edited with before/after diff

Resend link → audit row magic_link_resent

Revoke → investor signed out, status revoked, audit row attributed to admin

Edit financial-model assumption → audit row assumption_edited with before/after value

Create second admin from first admin's session → second admin logs in independently

Deactivate first admin from second admin's session → first admin's next request 401s

Existing curl with Bearer $PITCH_ADMIN_SECRET on /api/admin/invite still works

Investor flow /auth → / unaffected (no regression)

🤖 Generated with Claude Code