- docker-compose.yml: require ${POSTGRES_PASSWORD} for the postgres service
and the app container's DATABASE_URL. No default — compose refuses to start
without it, mirroring the existing PGADMIN_PASSWORD pattern.
- Dockerfile.prod: move auth/db ENV assignments from persistent ENV lines into
an inline env prefix on the `pnpm build` RUN step. Placeholders are still
available to `next build` but no longer persist in the builder layer or in
the published migrator image (which is FROM builder).
- Dockerfile.dev: add HEALTHCHECK against /api/health and install curl for it.
- .dockerignore: cover nested **/.env*, **/*.pem, **/*.key, **/secrets/**.
- runtime-env.ts: add the CI build placeholder strings to the disallowed-secret
set so a misconfigured prod deploy using the baked-in ARG defaults fails
startup instead of silently running with a known-bad secret.
- .env.example: document the new POSTGRES_PASSWORD requirement.
- CI: write POSTGRES_PASSWORD into the Fresh-Linux Docker Deploy job's .env
(must match docker-compose.ci.yml's hardcoded DATABASE_URL), and provide a
dummy value in the E2E job where compose validates all services' interp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- global-setup.ts: create reset-test@planarchy.dev directly via DB
(argon2id hash computed in Node.js, inserted via docker exec psql stdin
with correct camelCase quoted column names + createdAt/updatedAt;
ON_ERROR_STOP=1 so failures propagate rather than being swallowed)
- helpers.ts: resetPasswordViaApi now updates passwordHash directly in DB
(bypasses tRPC batch mutation format issues entirely);
getLatestEmailTo decodes MIME parts per Content-Transfer-Encoding
(quoted-printable soft line breaks were truncating 64-char tokens to ~14 chars)
- invite-flow.spec.ts: use fresh unauthenticated browser context for
the invite accept page (admin context was inheriting cookies)
- docker-compose.yml: hardcode SMTP_HOST=mailhog for Docker app service
(host .env value localhost doesn't reach Mailhog inside Docker network)
All 3 email E2E tests pass: invite flow, password reset flow, invalid token.
Co-Authored-By: claude-flow <ruv@ruv.net>
- SMTP: SMTP_HOST/PORT/USER/FROM/TLS now all have ENV override support
(previously only SMTP_PASSWORD was env-aware). ENV takes priority over DB.
- docker-compose.yml: forward all SMTP_* env vars to app container + add
Mailhog service (ports 1025 SMTP / 8025 HTTP, always available in dev)
- Password reset: PasswordResetToken Prisma model + authRouter with
requestPasswordReset (timing-safe, no email enumeration) + resetPassword
- UI: /auth/forgot-password, /auth/reset-password/[token] pages +
"Forgot password?" link on sign-in page
- E2E: Mailhog helpers (getLatestEmailTo, clearMailhog, extractUrlFromEmail)
+ invite-flow.spec.ts + password-reset.spec.ts
Co-Authored-By: claude-flow <ruv@ruv.net>
#41 (critical): Replace plain Error throws in authorize() with CredentialsSignin
subclasses (MfaRequiredError / MfaRequiredSetupError / InvalidTotpError).
Auth.js v5 forwards CredentialsSignin.code to the client via SignInResponse.code;
plain throws become CallbackRouteError and the message is never visible.
Signin page now checks result.code ?? result.error for exact code matching.
#38: MfaPromptBanner converted to fully client-side component via
trpc.user.getMfaStatus.useQuery() — disappears immediately after MFA enable
without requiring page reload. Snooze key remains userId-scoped via useSession().
Server-side prisma.user.findUnique call removed from (app)/layout.tsx.
#40: NEXTAUTH_URL default fallback removed from docker-compose.yml.
The variable is now required (:?) — docker compose up fails with a descriptive
error if the value is missing, preventing silent localhost redirect bugs.
Tests: auth.test.ts (5), MfaPromptBanner.test.ts (7), reset-password.test.ts (6)
All new tests green. pnpm --filter @capakraken/web exec tsc --noEmit clean.
Co-Authored-By: claude-flow <ruv@ruv.net>
#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>
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>
#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>