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

Merged
sharang merged 2 commits from feature/pitch-admin-ui into main 2026-04-07 10:36:16 +00:00
Owner

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

## 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](https://claude.com/claude-code)
sharang added 1 commit 2026-04-07 09:28:18 +00:00
feat(pitch-deck): admin UI for investor + financial-model management
Some checks failed
CI / Deploy (pull_request) Has been skipped
CI / go-lint (pull_request) Failing after 2s
CI / python-lint (pull_request) Failing after 11s
CI / nodejs-lint (pull_request) Failing after 2s
CI / test-go-consent (pull_request) Failing after 2s
CI / test-python-voice (pull_request) Failing after 9s
CI / test-bqas (pull_request) Failing after 8s
fc71439011
Adds /pitch-admin dashboard with real admin accounts (bcrypt) and full
audit attribution for every state-changing action.

Backend:
- pitch_admins + pitch_admin_sessions tables (migration 002)
- pitch_audit_logs.admin_id + target_investor_id columns
- lib/admin-auth.ts: bcryptjs hashing, single-session enforcement,
  jose JWT with 'pitch-admin' audience claim, requireAdmin guard
- logAudit extended to accept admin_id and target_investor_id
- middleware.ts: gates /pitch-admin/* and /api/admin/* on the admin
  cookie (with bearer-secret fallback for CLI compatibility)
- 14 API routes under /api/admin-auth and /api/admin (login, logout,
  me, dashboard, investors[id] CRUD + resend, admins CRUD,
  fm scenarios + assumptions PATCH)
- Existing /api/admin/{invite,investors,revoke,audit-logs} migrated
  to requireAdmin and now log with admin_id + target_investor_id
- scripts/create-admin.ts CLI bootstrap (npm run admin:create)

Frontend:
- /pitch-admin/login + /pitch-admin/(authed) route group
- AdminShell with sidebar nav + StatCard + AuditLogTable components
- Dashboard with KPIs, recent logins, recent activity
- Investors list with search/filter + resend/revoke inline actions
- Investor detail with inline edit + per-investor audit timeline
- Audit log viewer with actor/action/date filters + pagination
- Financial model scenario list + per-scenario assumption editor
  (categorized, inline edit, before/after diff in audit)
- Admins management (add, deactivate, reset password)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sharang added 1 commit 2026-04-07 09:39:22 +00:00
test(pitch-deck): vitest setup + tests for auth + admin-auth + rate-limit
Some checks failed
CI / go-lint (pull_request) Failing after 1s
CI / python-lint (pull_request) Failing after 10s
CI / nodejs-lint (pull_request) Failing after 2s
CI / test-go-consent (pull_request) Failing after 2s
CI / test-python-voice (pull_request) Failing after 9s
CI / test-bqas (pull_request) Failing after 12s
CI / Deploy (pull_request) Has been skipped
04ceed61c9
Adds vitest with 36 tests covering the security primitives:

- lib/auth: token gen uniqueness, hashToken determinism, JWT roundtrip,
  validateAdminSecret bearer flow, getClientIp x-forwarded-for parsing
- lib/admin-auth: bcrypt hash uniqueness/verify, JWT roundtrip,
  audience claim isolation (admin JWT does not validate as investor JWT)
- lib/rate-limit: limit enforcement, key isolation, window reset via
  fake timers, preset config sanity

Pure-function coverage only — route handler integration tests would
need a test DB and are deferred.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sharang merged commit c7ab569b2b into main 2026-04-07 10:36:16 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Benjamin_Boenisch/breakpilot-core#3