#28 - TOTP rate limiting (verifyTotp): added totpRateLimiter (10 req/30s),
throws TOO_MANY_REQUESTS before DB hit; 16 unit tests including rate-limit
exceeded + userId key isolation.
#29 - /api/reports/allocations role check: only ADMIN/MANAGER/CONTROLLER may
access; returns 403 otherwise; 9 unit tests (401 unauthenticated, 403 for
USER/VIEWER, 200 for allowed roles + xlsx format).
#31 - pgAdmin credentials moved out of docker-compose.yml into env vars;
PGADMIN_PASSWORD is now required (:?) to prevent accidental plaintext
exposure in committed files.
#34 - Server-side HTML sanitization for comment bodies via stripHtml():
strips all tags + decodes safe entities before persistence; 16 unit tests
covering passthrough, injection patterns, entity decoding.
#35 - MFA setup prompt banner (MfaPromptBanner): shown to ADMIN/MANAGER users
without TOTP enabled; user-scoped localStorage snooze (7 days); links to
/account/security; accessibility role=alert; 7 structural unit tests.
#33 - Auth anomaly alerting cron (/api/cron/auth-anomaly-check): detects
HIGH_GLOBAL_FAILURE_RATE and CONCENTRATED_FAILURES in 30-minute window;
CRITICAL notification to ADMINs; fail-closed via verifyCronSecret;
10 unit tests.
#32 - MFA enforcement policy: added requireMfaForRoles field to SystemSettings
schema + Prisma migration; auth.ts blocks login with MFA_REQUIRED_SETUP
signal if role is enforced but TOTP not set up; signin page redirects to
/account/security?mfa_required=1; settings schema + view model updated;
11 unit tests.
#30 - API keys architecture decision documented in LEARNINGS.md; no code
written — product decision required before implementation.
Co-Authored-By: claude-flow <ruv@ruv.net>
New users on a shared device were picking up a previous user's stale
(potentially empty) dashboard layout from localStorage because the key
"capakraken_dashboard_v1" was not user-scoped.
- useDashboardLayout: key is now capakraken_dashboard_v1_{userId};
userId is resolved via trpc.user.me before touching localStorage
- Initial state falls back to createDefaultDashboardLayout() until
userId resolves, then hydrates from the user-scoped key
- DB layout still wins over localStorage when it has data (unchanged)
- E2E test suite covers: new-user flow, modal widget list, add widget
persists after reload, cross-user localStorage isolation
- plan.md: added ticket #27 implementation plan
Co-Authored-By: claude-flow <ruv@ruv.net>
Three related fixes to prevent E2E test runs from disrupting real user sessions:
1. auth.ts: skip active_sessions registration in E2E mode
E2E logins now return early after setting token.sid without writing
to active_sessions. Prevents test sessions from kicking real user
sessions via the concurrent-session limit.
2. trpc/route.ts: skip active_sessions validation in E2E mode
Pairs with (1): if registration is skipped, validation must be too,
otherwise every storageState-based test gets a 401 "Session revoked".
3. docker-compose.yml: hardcode Docker-internal DATABASE_URL + E2E_TEST_MODE
Previously ${DATABASE_URL:-postgres:5432} picked up the host's
localhost:5433 override and passed it into the container, where
localhost refers to the container itself — breaking db:migrate:deploy
on container recreate. Now hardcoded to postgres:5432.
Also adds E2E_TEST_MODE=true to the dev container environment.
Result: 21/21 dev-system E2E tests pass, test runs leave zero footprint
in active_sessions and rate limiter counters for real user accounts.
The timeline disruption caused by test sessions kicking the admin's
real browser session is also resolved.
Co-Authored-By: claude-flow <ruv@ruv.net>
Fixes 8 failures from the first test run:
1. Rate limiter exhaustion (5/8 failures)
Admin was logged in 9× across the suite, hitting the 5/15min auth
limit. Fix: global-setup.ts logs in once per role and saves storage
state; all non-login tests use storageState so they skip the form.
Total admin logins per suite run: 3 (global setup + 2 explicit tests).
2. Strict-mode violations (2/8 failures)
toBeVisible() matched 3 email cells / 2 permission-error nodes.
Fix: .first() on both locators.
3. Auth.js v5 signout confirmation page (1/8 failures)
GET /auth/signout renders a confirm form rather than immediately
redirecting. Fix: signOut() helper clicks the submit button.
Note: running the suite right after a previous run may fail if the
in-memory rate limit hasn't reset (15-min window). Restart the dev
server, or add E2E_TEST_MODE=true to apps/web/.env.local to bypass.
Co-Authored-By: claude-flow <ruv@ruv.net>
Auth.js v5 manages token.jti internally and overwrites it after the jwt
callback. Storing our session UUID in token.sid ensures the value we
persist in active_sessions matches what the signed cookie carries.
- jwt callback: token.sid = jti (was token.jti)
- session callback: read from token.sid
- signOut event: falls back to token.jti for backward compat with any
sessions created before this change
Also adds Playwright dev-system test suite (playwright.dev.config.ts +
e2e/dev-system/) that validates login, session registry health, and
RBAC enforcement against the running localhost:3100 dev server.
Co-Authored-By: claude-flow <ruv@ruv.net>
The active_sessions table was never migrated to production — the model
was added to the Prisma schema via db push only. prisma migrate deploy
was a no-op because no migration directories existed.
Without the table, prisma.activeSession.findUnique() throws P2021,
crashing the tRPC handler with 500 on every authenticated request.
This silently emptied all admin pages (users, system-roles, etc.).
Changes:
- Wrap the jti ActiveSession lookup in try-catch so the tRPC handler
degrades gracefully (fail-open) if the table is temporarily missing
- Add packages/db/prisma/migrations/20260401000000_active_sessions/
so prisma migrate deploy creates the table on next production deploy
(idempotent via IF NOT EXISTS — safe if table already exists)
Co-Authored-By: claude-flow <ruv@ruv.net>
#19 MFA QR code: render locally via qrcode package, remove external qrserver.com request
#20 Webhook SSRF: add ssrf-guard.ts with DNS-verified IP blocklist; enforce on create/update/test/dispatch
#21 /api/perf: fail-closed when CRON_SECRET missing; remove query-string token auth
#22 CSP: remove unsafe-eval and unsafe-inline from script-src in production builds
#23 Active session registry: forward jti into session object; validate against ActiveSession on every tRPC request
#24 Docker: add missing packages/application to Dockerfile.dev; fix pnpm-lock.yaml glob;
run db:migrate:deploy on container start so a fresh checkout boots without manual steps
Also: fix pre-existing TS error in e2e/allocations.spec.ts (args.length literal type overlap)
Co-Authored-By: claude-flow <ruv@ruv.net>