April 8: Seed Accounts and Sanitized Messages
Ripping out the last of the hardcoded dev credentials, wiring the JWT secret through docker-compose, gating auth debug logs behind a development flag, and sanitizing the remaining places that were returning raw ex.Message to clients.
Developer Journal
Remove hardcoded seed credentials (#1063)
The seed data loader was creating three accounts (admin, demo, readonly) with passwords embedded as string literals in the .cs file. Convenient for first-run dev, terrible for anything that boots from a cloned repo in production mode.
Moved the seed credentials into configuration — Seed:Admin:Password, Seed:Demo:Password, Seed:ReadOnly:Password — defaulting to null. If null, the loader logs a warning and skips creating that account. If set (in appsettings.Development.json for local or via env vars for deploys), it creates with that password.
The production docker-compose sets the envs to random generated values the first time it runs, and the deploy docs (coming with #650) explain how to rotate them. Issue #1047 was the tracking bug; closed.
Wire JWT secret through docker-compose (#1065)
The JWT secret key was loaded from appsettings.json with a dev default of "a-very-long-dev-secret-that-is-not-for-production" and no env override wired in docker-compose. In production mode the backend was starting with the dev default because the env var existed but wasn't being passed into the container.
Added JWT_SECRET_KEY to the docker-compose environment block for the backend service and documented the generation step (openssl rand -base64 64) in the production deploy guide. On container start, the backend validates the secret is at least 32 bytes and refuses to boot with the dev default if ASPNETCORE_ENVIRONMENT=Production.
Gate auth debug logging behind development flag (#1066)
Auth middleware was logging token-parsing details at Information level — enough to be helpful when debugging a failed login, enough to be noise at Warning level in production logs. Gated behind IHostEnvironment.IsDevelopment() so production logs only get the Warning-level failures (with the scrubbed details from April 6) and Information events stay out of the hot logging path.
Sanitize ex.Message in error responses (#1068, #1069)
Two PRs to chase the remaining spots returning raw ex.Message to clients. Internal exception messages can leak connection strings, file paths, SQL, and stack-local variable values depending on what threw. #1068 replaced the obvious ones (controllers returning StatusCode(500, ex.Message)) with the generic error envelope from April 4's middleware. #1069 caught the ones that were wrapping ex.Message into a custom response type — easy to miss on grep because the variable was renamed.
All remaining controller paths now return { code, message: "Internal server error", traceId } with the trace ID cross-referenced to the server-side log that has the real message.
Frontend test coverage for error states (#1064)
The frontend test coverage audit (#1062) found that most error state branches had no tests — components rendered a loading state, rendered a success state, but nobody exercised "what happens when the fetch rejects." Added tests for error states and edge cases across the wizard flow, the target detail page, and the composite creation modal. Bumped frontend coverage another point or so, and the tests caught two actual bugs that were fixed in the same PR (a null deref when a job completed with zero steps, and an unhandled rejection when the SignalR connection died mid-poll).
What shipped
| PR | Title |
|---|---|
| #1069 | fix: sanitize ex.Message in remaining controller error responses |
| #1068 | fix: replace internal exception details with safe error messages |
| #1066 | fix: gate auth debug logging behind development-only flag |
| #1065 | fix: wire JWT secret key through docker-compose environment |
| #1064 | test: add missing frontend test coverage for error states and edge cases (#1062) |
| #1063 | fix: remove hardcoded seed credentials, load from config/env vars (#1047) |