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

@@ -20,11 +20,12 @@ type FinTab = 'overview' | 'guv' | 'cashflow'
interface FinancialsSlideProps {
lang: Language
investorId: string | null
}
export default function FinancialsSlide({ lang }: FinancialsSlideProps) {
export default function FinancialsSlide({ lang, investorId }: FinancialsSlideProps) {
const i = t(lang)
const fm = useFinancialModel()
const fm = useFinancialModel(investorId)
const [activeTab, setActiveTab] = useState<FinTab>('overview')
const de = lang === 'de'
@@ -268,6 +269,26 @@ export default function FinancialsSlide({ lang }: FinancialsSlideProps) {
{de ? 'Berechne...' : 'Computing...'}
</div>
)}
{/* Snapshot status + reset */}
{investorId && (
<div className="flex items-center justify-between mt-2 pt-2 border-t border-white/5">
<span className="text-[9px] text-white/30">
{fm.snapshotStatus === 'saving' && (de ? 'Speichere...' : 'Saving...')}
{fm.snapshotStatus === 'saved' && (de ? 'Ihre Aenderungen gespeichert' : 'Your changes saved')}
{fm.snapshotStatus === 'restored' && (de ? 'Ihre Werte geladen' : 'Your values restored')}
{fm.snapshotStatus === 'default' && (de ? 'Standardwerte' : 'Defaults')}
</span>
{fm.snapshotStatus !== 'default' && (
<button
onClick={() => fm.activeScenarioId && fm.resetToDefaults(fm.activeScenarioId)}
className="text-[9px] text-white/40 hover:text-white/70 transition-colors"
>
{de ? 'Zuruecksetzen' : 'Reset to defaults'}
</button>
)}
</div>
)}
</div>
</FadeInView>
</div>