Repository navigation
Description
Owner idea from the #280 discussion. Follow-on to #280 (which moves Demo/Simulation seeding off the boot path into a command but leaves the base DatabaseSeeder as-is). This issue reworks the base provisioning so a fresh deploy is usable with no Seed:* config and no publicly-known credential.
Goal
Retire the runtime base DatabaseSeeder + the Seed:Enabled/Seed:AdminEmail/Seed:AdminPassword config surface. A fresh, migrated database should come up already usable, and the first admin should be forced to set their own password — without ever shipping a known credential.
Two clearly-separable parts
1. Static reference data → into the schema (low risk, clear win)
Roles (Admin/Manager/Sales/ReadOnly), the 10 default egg grades, and the default account are static reference data. Move them into the InitialCreate migration via migrationBuilder.InsertData(...) (or a dedicated data migration). No runtime seeder, no config, deterministic, multi-instance-safe by construction.
2. First admin → secure first-run, NOT a baked-in credential (the careful part)
A user row needs a password hash, so baking an admin into the migration means every deployment ships the same publicly-known admin credential (the hash is in the OSS repo). "Force a change on first login" fixes the post-first-login state but leaves the deploy→first-login window exposed with a repo-known password — a regression vs today's operator-supplied secret for the internet-facing prod #244 targets. So do NOT hardcode a credential. Options (decide during design):
- Generated-at-first-run secret: on first boot against an un-provisioned DB, generate a random admin password, create the admin, and surface it once to the operator (container log line and/or a one-time setup token/URL) — the pattern GitLab/Grafana/etc. use. Nothing known ever ships.
MustChangePasswordon top: a domain flag on the admin user, an auth gate that forces a password change before any other action, and a first-login "set your password" screen in the SPA. New surface: domain (User.MustChangePassword), the login/refresh flow, and a SPA screen.- Keep an operator-supplied override (env) for those who prefer to inject their own admin secret.
Blast radius / pieces
- EF migration with
InsertDatafor roles/grades/account (+ aSecurityReviewedcheck that no credential/hash is embedded). - A first-run admin bootstrap (generate + surface secret) — as a one-shot command sibling of Extract DB seeding (Demo/Simulation) out of API startup into an explicit command entry point #280's
seed/migrate, NOT a serving-boot side effect. MustChangePassword: domain flag + auth gate + SPA first-login password screen (+ Vitest).- Retire
Seed:Enabled/AdminEmail/AdminPassword; updatedeploy/.env(.example), compose, and docs; the integration-test helpers that currently create accounts directly are unaffected. - Ships with the standing rule: every user-visible change updates GLOSSARY.md + the SPA Help page.
Why its own issue (not #280)
#280 moves the seed invocation; this changes what the base data is and how the first admin is secured — with real security (#244) + auth-flow + SPA-UX implications. It deserves a design pass (likely a brainstorm on the first-run-secret mechanism) before implementation.
Refs
- Extract DB seeding (Demo/Simulation) out of API startup into an explicit command entry point #280 (moves Demo/Sim seeding to a command; leaves base seeder — this replaces it), Deployment readiness audit: production go-live scan before first real users (host undecided) #244 (deployment-readiness / prod credential story), Squash the 30 EF migrations into a single InitialCreate before first production deploy #245 (
MigrateOnStartup+ one-shot init posture), F12: Demo seed data — Seed:Demo flag for dev/demo environments #58 (demo data), F18: In-app help — user guide + glossary in the SPA #71 (Help page), Tech debt: magic strings — duplicated role/claim constants, untyped SPA status strings #84 (magic-strings/config surface).
Post-login privilege boundary — 2026-07-31
This issue secures first provisioning and forced first-password change. It does not address a stolen, already-authenticated Owner token creating a durable replacement Owner or resetting another privileged password. Recent step-up authentication for those post-login operations is tracked separately in #308.
- Keep first-run setup tokens/passwords and browser step-up grants as separate credential types, audiences, lifetimes, and revocation paths.
- Do not close Require recent step-up authentication for privileged user administration #308 as covered by MustChangePassword or generated first-run credentials.
Activity
mforce commentedon Aug 1, 2026
Design decision — first-run admin mechanism (owner, 2026-08-01)
Both parts of this issue are in scope, and the first admin is provisioned by generated-at-first-run secret plus
MustChangePassword.Part 1 — static reference data into the schema. Roles (
Admin/Manager/Sales/ReadOnly), the 10 default egg grades, and the default account move into the migration viamigrationBuilder.InsertData(...). Deterministic, multi-instance-safe, no runtime seeder, no config.Part 2 — first admin. No credential is ever baked into the repo. On first provisioning against an un-provisioned database, a one-shot command generates a random admin password, surfaces it to the operator once on stdout (never the logger/OTLP — same rule as the
recover-adminverb in #265), and setsUser.MustChangePassword. An auth gate forces a password change before any other action, with a first-login "set your password" screen in the SPA.Rejected: baking a credential (ships a repo-known password); generated secret without the forced change (a printed password can sit unchanged indefinitely); keeping an env override (retains a slice of the
Seed:*surface this issue exists to retire — revisit only if an operator actually asks).Scope reminders carried from the issue body:
migrate/seed/recover-admin— never a serving-boot side effect.Seed:Enabled/Seed:AdminEmail/Seed:AdminPassword; updatedeploy/.env(.example), compose, and docs.MustChangePasswordis a new user-visible security behaviour: GLOSSARY.md and the SPA Help page update in the same PR, and the SPA screen ships with Vitest tests.Scheduling: this is schema-affecting, so it lands before the #245 InitialCreate squash and before the pre-launch schema freeze. Needs a milestone and an epic-15 checklist entry (currently has neither).
fix(api): idempotent base-reference-data migration + bootstrap-admin …
feat(api,web): base provisioning via migration + secure first-run adm…
fix(api): idempotent base-reference-data migration + bootstrap-admin …
test(api): adapt multi-instance and legacy-row tests to #283 provisio…
fix(comments): correct HasData/InsertData references to describe actu…
fix(api,docs): PR #339 review findings — error re-execution gate, pas…
feat(api,web): base provisioning via migration + secure first-run adm…
fix(api): idempotent base-reference-data migration + bootstrap-admin …
test(api): adapt multi-instance and legacy-row tests to #283 provisio…
fix(comments): correct HasData/InsertData references to describe actu…
fix(api,docs): PR #339 review findings — must-change-password error g…
12 remaining items
ci(e2e): derive the service list, and stop parsing workflow commands …
ci(e2e): keep the stack logs a Playwright failure needs to be diagnos…
fix(api,web): correct the disabled-Owner advice and explain system ac…
fix(api,web): correct the disabled-Owner advice and explain system ac…
fix(api): seeded demo and simulation records name a real person (#517)
fix(tests): update the tenant-bypass allow-list for the new signature (…
fix(api): print the farm code from bootstrap-admin (#589) (#594)
docs: fix k6 prep ordering and rate-limit cleanup in the fixture runbook
docs: fix k6 prep ordering and rate-limit cleanup in the fixture runbook
docs: add k6 preparation steps to the dev-database fixture runbook (#643
feat(api): provision-account takes an optional --timezone at creation (…
fix: read the switch fresh, announce the consent refusal, classify th…