Commit Graph

772 Commits

Author SHA1 Message Date
Hartmut 745be7ee8b fix(dashboard): scope localStorage key by userId to prevent cross-user layout bleed (#27)
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>
2026-04-01 22:44:41 +02:00
Hartmut d3bfa8ca98 test(mfa): full MFA test coverage — unit + E2E
Unit tests (packages/api — 13 tests):
- generateTotpSecret: DB write, returns secret + uri
- verifyAndEnableTotp: valid token enables; invalid/already-enabled/no-secret guards
- verifyTotp (login): valid → ok; invalid → UNAUTHORIZED; not-enabled → BAD_REQUEST
- getCurrentMfaStatus: reads totpEnabled flag

E2E tests (apps/web/e2e/dev-system/mfa.spec.ts — 7 scenarios):
- Setup flow: generate secret, enable with valid code, reject invalid code, UI QR check
- Login flow: MFA prompt appears, valid code logs in, wrong code shows error + stays on prompt
- Login without MFA: no TOTP prompt for users without MFA enabled

Also: start.sh health-check timeout 30s → 90s (container startup can exceed 30s)

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-01 22:30:36 +02:00
Hartmut bfdf0a82da security/platform: close audit findings #19–#26
Tests, CSP nonce middleware, SSRF guard, perf-route hardening,
Docker env isolation, migration runbook, RBAC E2E coverage.

Tickets resolved:
- #19: MfaSetup.test.ts — static source tests confirming local QR rendering
- #20: ssrf-guard.test.ts (16 tests) + webhook-procedure-support mock fix
- #21: /api/perf route.test.ts (5 tests) — header-only auth, fail-closed
- #22: middleware.ts (nonce-based CSP) + middleware.test.ts (6 tests);
       layout.tsx async + nonce prop; CSP removed from next.config.ts
- #23: Active-session registry enforcement verified (already in codebase)
- #24: docker-compose.yml REDIS_URL hardcoded (no host-env substitution)
- #25: docker-compose.yml REDIS_URL + docs/developer-runbook.md created
- #26: e2e/dev-system/rbac-data-access.spec.ts (12 tests, 3 roles × 4 procedures)

Quality gates: tsc clean, api 1447/1447, web 189/189 passing.
Turbo concurrency capped at 2 (package.json) to prevent OOM under
parallel test runs.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-01 22:14:20 +02:00
Hartmut 4901bc878b fix(e2e): complete E2E_TEST_MODE isolation for session registry + rate limits
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>
2026-04-01 20:57:14 +02:00
Hartmut 8429bd86d4 test(e2e): fix dev-system test suite — storageState + strict-mode + signout
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>
2026-04-01 19:09:49 +02:00
Hartmut 3d8a256d52 fix(auth): use token.sid to avoid Auth.js jti claim conflict
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>
2026-04-01 19:00:44 +02:00
Hartmut a867672afa Merge remote-tracking branch 'origin/main'
# Conflicts:
#	apps/web/src/components/allocations/AllocationsClient.tsx
2026-04-01 18:41:54 +02:00
Hartmut 5bc7cace26 fix(auth): make active-session check fail-open; add missing DB migration
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>
2026-04-01 18:38:05 +02:00
Hartmut 0e119cfe73 security: close audit findings #19–#23 and harden Docker setup (#24)
#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>
2026-04-01 18:19:21 +02:00
Hartmut 57ea9d8310 fix(allocations): recover from fully filtered empty state 2026-04-01 15:18:08 +02:00
Hartmut b841cc9127 fix(allocations): expand grouped rows by default 2026-04-01 15:18:07 +02:00
Hartmut fd75628e9d fix(allocations): recover from fully filtered empty state 2026-04-01 15:16:57 +02:00
Hartmut 7df751d5eb fix(allocations): expand grouped rows by default 2026-04-01 15:13:24 +02:00
Hartmut 4b14db9dc6 fix(timeline): pause sse while hidden 2026-04-01 15:05:34 +02:00
Hartmut 3258b59e21 fix(timeline): resync after sse reconnect 2026-04-01 15:04:00 +02:00
Hartmut d4652b7a42 fix(timeline): cancel stranded drag interactions 2026-04-01 14:57:56 +02:00
Hartmut a71bbeb640 fix(timeline): stabilize overlay lifecycle 2026-04-01 14:41:03 +02:00
Hartmut fa5e654739 fix(timeline): harden project view interactions 2026-04-01 14:10:28 +02:00
Hartmut e103174d39 refactor(web): extract preview target setup 2026-04-01 11:59:10 +02:00
Hartmut 2a7769a0de refactor(web): extract range release resolution 2026-04-01 11:53:11 +02:00
Hartmut 1e2bd3d4eb refactor(web): extract project drag finalize 2026-04-01 11:49:14 +02:00
Hartmut 463caedcfd refactor(web): extract touch event forwarding 2026-04-01 11:39:39 +02:00
Hartmut 37c6e03d23 refactor(web): extract allocation release effects 2026-04-01 11:35:17 +02:00
Hartmut f4e9831dea refactor(web): extract allocation drag session 2026-04-01 11:27:03 +02:00
Hartmut 510459fbff refactor(web): extract allocation multi-drag session 2026-04-01 11:22:18 +02:00
Hartmut 5402189158 refactor(web): extract drag position helpers 2026-04-01 11:18:31 +02:00
Hartmut 3fe3a5fb2a refactor(web): extract project drag session 2026-04-01 11:16:15 +02:00
Hartmut 0181f2b304 refactor(web): extract multi-select session 2026-04-01 11:14:28 +02:00
Hartmut b14be80e32 refactor(web): extract timeline drag cleanup 2026-04-01 11:12:20 +02:00
Hartmut 922394c56a refactor(web): split touch canvas adapters 2026-04-01 11:09:26 +02:00
Hartmut a4789d718b refactor(web): centralize multi-select release handling 2026-04-01 10:50:21 +02:00
Hartmut ca947befde refactor(web): extract allocation release classification 2026-04-01 10:48:47 +02:00
Hartmut 0ab1374853 refactor(web): centralize touch mouse adapters 2026-04-01 10:43:38 +02:00
Hartmut eda8722d83 refactor(web): extract document drag listeners 2026-04-01 10:39:28 +02:00
Hartmut 84c5760392 refactor(web): extract range selection bootstrap 2026-04-01 10:17:39 +02:00
Hartmut c941b1e5cf refactor(web): extract allocation drag action plans 2026-04-01 10:15:54 +02:00
Hartmut 203bb8751d refactor(web): extract allocation drag bootstrap 2026-04-01 10:10:06 +02:00
Hartmut 892a9c5ccf refactor(web): extract project drag helpers 2026-04-01 10:06:32 +02:00
Hartmut c32f56ba89 refactor(web): extract allocation multi-drag helpers 2026-04-01 10:03:16 +02:00
Hartmut e23b502dd9 test(repo): guard allocation drag helper boundaries 2026-04-01 09:58:20 +02:00
Hartmut 6dac993521 refactor(web): extract allocation drag finalize helpers 2026-04-01 09:57:29 +02:00
Hartmut 54c6cf2e2d refactor(web): extract optimistic timeline reconciliation 2026-04-01 09:53:40 +02:00
Hartmut ea4074af8f test(repo): guard timeline drag helper boundaries 2026-04-01 09:52:23 +02:00
Hartmut 848797b4d2 refactor(web): extract timeline range selection helpers 2026-04-01 09:51:18 +02:00
Hartmut 43f04d66c8 refactor(web): extract timeline multi-select helpers 2026-04-01 09:50:03 +02:00
Hartmut 3abb3bc865 refactor(web): extract timeline touch helpers 2026-04-01 09:48:04 +02:00
Hartmut 167eec31de test(repo): guard timeline live preview boundaries 2026-04-01 09:43:14 +02:00
Hartmut 5e8babd1e6 test(web): cover timeline live preview render edges 2026-04-01 09:41:43 +02:00
Hartmut 5011d071b8 refactor(web): extract timeline live preview helpers 2026-04-01 09:40:07 +02:00
Hartmut 2855567456 test(web): cover timeline project row layout 2026-04-01 09:29:43 +02:00