- Password validation: min(8) → min(12) across auth.ts, user-procedure-support.ts,
and invite.ts (aligns with NIST SP 800-63B modern recommendations)
- Error boundary: stop rendering raw error.message which could leak internal
details; always show the generic fallback text
- Add `pnpm audit` script (--audit-level=high) for dependency vulnerability scanning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds lastTotpAt timestamp to User model. After a successful TOTP validation,
the timestamp is recorded. Any reuse of the same code within the 30-second
window is rejected as a replay attack.
verifyTotp now returns a single generic UNAUTHORIZED error regardless of
whether the user ID is invalid or TOTP is not enabled, preventing enumeration
of user IDs and MFA status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blueprint rolePresets: cap array at 100 items to prevent storage abuse
- notification CreateManagedNotification: add .max() on title (500),
body (2000), type (100), entityType/entityId (200), link (1000),
taskAction (200)
- settings: add .max() on all string config fields; add regex allowlist
(/^[a-zA-Z0-9._-]+$/) on model name fields (geminiModel,
azureDalleDeployment, azureOpenAiDeployment) to prevent path manipulation
- sanitizeHtml: fix SSR bypass — server-side branch now strips HTML tags
instead of returning the raw string unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- setUserPassword and resetPassword now call activeSession.deleteMany after
updating the passwordHash, so any pre-change sessions are immediately revoked
(CWE-613 session fixation after credential change)
- setUserPermissions and resetUserPermissions now use explicit Prisma select to
exclude passwordHash and totpSecret from the returned user object
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move core entitlement business logic (syncEntitlement, balance reading,
year summary, set/bulk-set) into packages/application/src/use-cases/entitlement/
using the deps-injection pattern. Audit logging stays in the router support
file; authorization check for getBalance/getBalanceDetail stays in the router
layer. The router support file becomes a thin wiring adapter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add deletedAt DateTime? to User, Client, Role, Resource, and Blueprint
models for GDPR-compliant deactivation audit trail. Soft-delete mutations
now stamp deletedAt: new Date() on deactivation and clear it on
reactivation. Migration and test assertions updated accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P2002/P2025/P2003 now map to CONFLICT/NOT_FOUND/BAD_REQUEST with generic
messages. Raw Prisma error details no longer reach the client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests expected include: { resourceRoles } but the Prisma select audit
changed the query to select: { ...RESOURCE_LIST_SELECT, resourceRoles }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces full model includes with field-scoped selects on the resource
list (listStaff) query. Avoids fetching large JSONB columns
(availability, valueScoreBreakdown) and unused scalar fields (aiSummary,
portfolioUrl, fte, resourceType, postalCode, etc.) when only
identity/rate fields are needed.
Adds RESOURCE_LIST_SELECT constant to packages/api/src/db/selects.ts
covering all fields actually consumed by ResourcesClient, FillOpenDemandModal,
EstimateWizard, EstimateWorkspaceDraftEditor, and ScenarioPlanner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Procedures were re-fetching the acting user from DB using the session
email, which breaks if email changes between session creation and request.
ctx.dbUser is populated by protectedProcedure and is always current.
Also removed the now-unused findVacationActor helper function.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Throw at startup in production if REDIS_URL/DATABASE_URL/NEXTAUTH_SECRET missing
- Warn in development when REDIS_URL falls back to localhost
- QueryClient: add gcTime, disable refetchOnWindowFocus, skip retry on 4xx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents mutations from committing without an audit trail if the
auditLog.create call fails after the main write already succeeded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves read, assignment-procedures, assignment-mutations, and demand
procedures into allocation/ so the domain boundary is discoverable
without grep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dailyCostCents, hoursPerDay, percentage now validated at API boundary
- vacation router no longer uses ctx.db as any
- scenarioData reads through typed Zod schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sends a Monday digest to all ADMIN + MANAGER users with:
- Team utilization % for the next 4 weeks
- Overbooked resource count
- Open demand count
- Upcoming vacation count
- Top 5 most utilized resources
Route: GET /api/cron/weekly-digest (secured by CRON_SECRET).
HTML template and plain-text fallback included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New allocation.checkConflicts managerProcedure: returns per-day overbooking
breakdown (availableHours, existingHours, requestedHours, overageHours,
maxOverbookPercent) plus vacation overlap list for the requested period.
Read-only — used by AllocationModal for pre-submission warnings.
- createAssignment(): replace the hard >5-day overbooking block with a soft
CONFLICT error. When allowOverbooking: true is passed the assignment is
created and overbookingAcknowledged is set to true on the record.
- allowOverbooking field added to CreateAssignmentBaseSchema (optional)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- event-bus: wrap each subscriber.fn call in try/catch so one throwing subscriber cannot kill delivery to all others
- event-bus: log Redis parse errors instead of swallowing them silently; add .catch() on Redis publish promise for async fallback to local delivery
- pruning.ts: new runPruning() deletes expired invite tokens, expired password-reset tokens, and read notifications older than 90 days
- settings.runPruning: expose pruning as adminProcedure mutation
- trpc.ts: E2E_TEST_MODE rate-limit bypass is now a no-op in production (NODE_ENV=production); logs a startup warning if misconfigured
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- applyProjectScenario: wrap assignment loop in db.$transaction to prevent partial updates
- vacation approve/reject: fix TOCTOU race via updateMany with status-guard in WHERE + CONFLICT on count=0
- vacation cancel: wrap vacation.update + entitlement.updateMany in $transaction
- batchApprove: collect mutations, wrap in $transaction, dispatch SSE/notifications after commit
- Fix dead-code bug in createHappyPathDb where $transaction was assigned after return
- Add atomicity and concurrency tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded dates (2026-03-20 / 2026-04-05) were now in the past, causing
the demand window filter (endDate >= now) to exclude the mock demand
requirement and miss the expected staffing anomaly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All chapter text inputs now show autocomplete suggestions from the
database (distinct chapter values from active resources) via HTML
<datalist> wired to trpc.resource.chapters:
- ResourceModal: chapter input
- RateCardsClient: rate card line chapter input
- EffortRulesClient: effort rule chapter input
- ExperienceMultipliersClient: replaces hardcoded CHAPTER_PRESETS
with live data, falls back to presets when no data available
Also revert blueprintRolePresetsInputSchema to z.array(z.unknown())
to restore compatibility with StaffingRequirement[] call sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add batchHardDelete adminProcedure to resource-mutations router
- Per-row Delete button visible to ADMIN role only
- Delete Selected button in BatchActionBar for ADMIN role only
- Two-step confirmation dialogs with permanent-action warnings
- Audit log written for each deleted resource
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds a transactional hard-delete procedure behind adminProcedure that
removes a resource's assignments and vacations first, then the record
itself, and writes an audit log entry. The ResourceModal exposes a
"Delete Resource" button (edit mode, ADMIN role only) with an inline
confirm step before the mutation fires.
Co-Authored-By: claude-flow <ruv@ruv.net>
Four new test files — 27 tests total:
- role-router-auth.test.ts (8): UNAUTHORIZED/FORBIDDEN on all mutations for
unauthenticated/USER callers; MANAGER and ADMIN happy paths
- webhook-router-auth.test.ts (6): adminProcedure guard verified for all
six webhook procedures across USER/MANAGER/ADMIN roles
- comment-sanitization-router.test.ts (4): proves stripHtml runs before
db.comment.create — script tags stripped, plain text and @mentions preserved
- auth-anomaly-check/route.test.ts (+5 unit tests): detectAuthAnomalies()
unit coverage — empty window, global threshold, per-entity threshold, null
entityId, and both anomaly types firing simultaneously
Co-Authored-By: claude-flow <ruv@ruv.net>
- useTimelineDrag: onProjectBarMouseDown and single-alloc drag path now reset
multiSelectRef + multiSelectState before starting a new drag, so the
FloatingActionBar is dismissed immediately when an unrelated drag begins
- FloatingActionBar.test.tsx: 4 regression tests for the null-render guard
(count=0) and all three label variants
- useTimelineSSE.test.ts: 2 new tests — tab hides during pending reconnect
timer (clears timer, resyncs on next open) and first-ever connection fails
before any open (retry open still resyncs correctly)
- assistant-tools-user-admin-inventory-read.test.ts: add isActive to expected
findMany select shape (already in production, test was stale)
Co-Authored-By: claude-flow <ruv@ruv.net>
- Invite flow: admin can invite users by email with role selection; accept-invite page
sets password and creates the account; 72-hour token expiry; E2E tests
- User deactivate/reactivate/delete: new tRPC procedures + UI buttons; deactivation
revokes all active sessions immediately; delete cascades vacation/broadcast records;
isActive field added via migration 20260402000000_user_isactive
- Auth: block login for inactive users with audit entry
- Favicon: SVG favicon + ICO/PNG fallbacks (16, 32, 180, 192, 512px); manifest updated
- Dashboard: GridLayout dynamic-import loading skeleton prevents blank dark area
on first login before react-grid-layout chunk is cached
- Admin users: remove max-w-5xl constraint so table uses full page width
- Dev: docker container restart workflow documented in LEARNINGS.md; Prisma generate
must run inside the container after schema changes (named node_modules volume)
Co-Authored-By: claude-flow <ruv@ruv.net>
Introduce getAppBaseUrl() in packages/api/src/lib/app-base-url.ts:
- Reads NEXTAUTH_URL (trimmed, trailing slash stripped)
- production: throws if NEXTAUTH_URL is missing/empty so broken
localhost links in emails are caught at runtime, not silently sent
- development/test: falls back to http://localhost:3100 with a
one-time console.warn
Replace the duplicated inline fallback in:
- packages/api/src/router/invite.ts (invite email link)
- packages/api/src/router/auth.ts (password reset email link)
Extend GET /api/health to report:
"baseUrl": { "configured": bool, "isLocalhost": bool }
so deployment checks can detect a misconfigured NEXTAUTH_URL.
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>
#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>