feat(pitch-deck): passwordless investor auth, audit logs, snapshots & PWA (#2)
All checks were successful
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 25s
CI / test-bqas (push) Successful in 27s
CI / Deploy (push) Successful in 6s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped

Adds investor-facing access controls, persistence, and PWA support to the pitch deck:

- Passwordless magic-link auth (jose JWT + nodemailer SMTP)
- Per-investor audit logging (logins, slide views, assumption changes, chat)
- Financial model snapshot persistence (auto-save/restore per investor)
- PWA support (manifest, service worker, offline caching, branded icons)
- Safeguards: email watermark overlay, security headers, content protection,
  rate limiting, IP/new-IP detection, single active session per investor
- Admin API: invite, list investors, revoke, query audit logs
- pitch-deck service added to docker-compose.coolify.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #2.
This commit is contained in:
2026-04-07 08:48:38 +00:00
parent 3a2567b44d
commit 645973141c
35 changed files with 4232 additions and 14 deletions

View File

@@ -0,0 +1,79 @@
-- =========================================================
-- Pitch Deck: Investor Auth, Audit Logs, Snapshots
-- =========================================================
-- Invited investors
CREATE TABLE IF NOT EXISTS pitch_investors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
company VARCHAR(255),
invited_by VARCHAR(255) NOT NULL DEFAULT 'admin',
status VARCHAR(20) NOT NULL DEFAULT 'invited'
CHECK (status IN ('invited', 'active', 'revoked')),
last_login_at TIMESTAMPTZ,
login_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pitch_investors_email ON pitch_investors(email);
CREATE INDEX IF NOT EXISTS idx_pitch_investors_status ON pitch_investors(status);
-- Single-use magic link tokens
CREATE TABLE IF NOT EXISTS pitch_magic_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
investor_id UUID NOT NULL REFERENCES pitch_investors(id) ON DELETE CASCADE,
token VARCHAR(128) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pitch_magic_links_token ON pitch_magic_links(token);
CREATE INDEX IF NOT EXISTS idx_pitch_magic_links_investor ON pitch_magic_links(investor_id);
CREATE INDEX IF NOT EXISTS idx_pitch_magic_links_expires ON pitch_magic_links(expires_at);
-- Audit log for all investor activity
CREATE TABLE IF NOT EXISTS pitch_audit_logs (
id BIGSERIAL PRIMARY KEY,
investor_id UUID REFERENCES pitch_investors(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
details JSONB DEFAULT '{}',
ip_address INET,
user_agent TEXT,
slide_id VARCHAR(50),
session_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pitch_audit_created ON pitch_audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_pitch_audit_investor ON pitch_audit_logs(investor_id);
CREATE INDEX IF NOT EXISTS idx_pitch_audit_action ON pitch_audit_logs(action);
-- Per-investor financial model snapshots (JSONB)
CREATE TABLE IF NOT EXISTS pitch_investor_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
investor_id UUID NOT NULL REFERENCES pitch_investors(id) ON DELETE CASCADE,
scenario_id UUID NOT NULL,
assumptions JSONB NOT NULL,
label VARCHAR(255),
is_latest BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pitch_snapshots_investor ON pitch_investor_snapshots(investor_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pitch_snapshots_latest
ON pitch_investor_snapshots(investor_id, scenario_id) WHERE is_latest = true;
-- Active sessions
CREATE TABLE IF NOT EXISTS pitch_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
investor_id UUID NOT NULL REFERENCES pitch_investors(id) ON DELETE CASCADE,
token_hash VARCHAR(128) NOT NULL,
ip_address INET,
user_agent TEXT,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pitch_sessions_investor ON pitch_sessions(investor_id);
CREATE INDEX IF NOT EXISTS idx_pitch_sessions_token ON pitch_sessions(token_hash);