Repository navigation
fix(admin): admin migrate sends the bootstrap admin's invite, again once its link expires - #102
Conversation
8238e84 to
3589e49
Compare
…nce its link expires Rebuilt on main, where #113 already has admin migrate send the bootstrap invite once. What this keeps from the original PR is the part main lacks: InviteBootstrapAdmin resends once the last invite is older than the link lifetime (inviteTTL = auth.DefaultInviteTTL, also set as the admin auth instance's InviteTTL so the two cannot drift), so a first email that never arrived or expired heals on a later deploy, and it never invites a disabled pending admin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3589e49 to
57ac5f3
Compare
|
Rebuilt on main (now 4523a36). Most of this PR was already merged by #113 (db2867d "admin migrate sends the first-admin invite once":
Proof on the rebased binary (scratch admin database, dev mailer): deploy 1 |
admin migrateseededADMIN_BOOTSTRAP_EMAILwithout a password and said "send its set-password link withadmin invite". The admin image is distroless, so a deployed service has no shell to runadmin invitein, and the Admins page needs an admin already. A fresh deployment had no way to get its first admin.Summary
AdminService.InviteBootstrapAdmin(operator only) sends the link as an auditedadmin.invitemutation while the admin is pending (no password, not disabled) and no invite is on record from the last link lifetime (72 h,auth.DefaultInviteTTL). The latest invite is found under the admin's id, then its email.RESEND_API_KEY(sends run in the background and a rejection is only logged) and an invite nobody opened in time.cmd/adminsets the admin auth instance'sInviteTTLto the same constant it passes, so the window and the link lifetime can't drift apart.Docs (README,
.env.example, CONTEXT, Makefile help, config comment) now describe the rule. Only the bootstrap invite is in scope: no password reset or 2FA recovery here.Evidence
Before (
main, sandbox Postgres, dev mailer): migrate seeds the admin and sends nothing, on every deploy.After, the built binary against a fresh admin database. Between deploys 2 and 3 the invite row was moved 73 h into the past (append-only trigger disabled for that
UPDATEon the scratch database):On an admin database seeded by
main's migrate (pending, never invited), the next migrate sent its invite and the one after sent nothing (first revision of this PR, same rule for "never invited").Tests:
TestInviteBootstrapAdmin(mocked ports):ErrAdminNotFoundwith the outcome recorded.TestServicesRefuseUnauthorizedActorscovers it as operator-only. Mutation check: forcing "never resend" failslast invite expired, and forcing "always resend" fails with an unexpectedBegin. Before the fix the test does not build (svc.InviteBootstrapAdmin undefined).Checks:
make check-fast✅,make generate lint✅ (no generated diff),make test✅ (-race, admin services 100 %, all coverage floors pass). The host OOM-killed some runs; the results above are full reruns of the same tree.Merge Danger
Door: two-way. Revert restores the old migrate. Invites already sent stay in the append-only audit log, which is harmless.
Blast Radius: admin. It touches only
admin migrateand one new operator-only service method. A deployment withADMIN_BOOTSTRAP_EMAILset whose admin is still pending gets at most one invite email per 72 h, sent from deploys. That includes existing deployments whose bootstrap admin was never invited, which get one on their next deploy.🤖 Generated with Claude Code