feat(users): Phase 7b — admin user management + email invitations - #174
Merged
Conversation
After bootstrap_admin, an admin had no way to add a second user: the
users API was GET-only. This completes org administration on the
Phase 7a token/mailer foundation.
Backend (services/lims/app/api/users.py):
- POST /api/users/ (admin only): creates an ACTIVE, passwordless user
and emails a 7-day single-use set-password link (the shared
/reset-password page terminates both reset and invite flows via
&welcome=1). Duplicate email in org → 409. Plain-str email shape
check, NOT pydantic EmailStr — that type imports email-validator,
which the lims image doesn't ship (found live: reload crash).
- PATCH /api/users/{id} (admin only, own org): name/role/is_active.
Self-protection: you cannot demote or deactivate your own account,
so an org can never brick its last admin. Cross-org ids are 404.
- issue_password_token imported RELATIVELY (.auth): the package is
`app.api` in the container and `services.lims.app.api` under tests —
the absolute spelling 500'd in the container (found live).
Frontend (/system/users):
- Invite dialog (email/name/role) on the real POST; role select +
activate/deactivate in the details dialog on the real PATCH; backend
errors (409, self-protection 400s) surface verbatim; react-query
invalidation keeps the list fresh.
Plumbing:
- compose lims env: SMTP_* + APP_BASE_URL passthrough (unset = honest
log-only dev mode).
- k8s lims Deployment: APP_BASE_URL + SMTP_* from the OPTIONAL
smtp-credentials secret (template + runbook decision row added) —
the platform deploys and runs without email, it just logs instead.
Proof: 10 new API tests (invite lifecycle incl. completion via the
reset endpoint, RBAC 403s, 409, self-protection 400s, cross-org 404) —
lims suite 90 passed. LIVE on the dev stack: invite 201 → link from
log → set password 204 → invitee logs in 200; engineer invite 403.
tsc clean; smoke 4/4; compose config valid; overlays render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
After
bootstrap_admin, an admin had no way to add a second user — the users API was GET-only. Second of the two Phase 7 (account lifecycle) PRs, building on #173's token + mailer foundation.What
Backend
POST /api/users/(admin only) — creates an active, passwordless user and emails a 7-day single-use set-password link; the shared/reset-passwordpage terminates both flows (&welcome=1). Duplicate email in org → 409.PATCH /api/users/{id}(admin only, own org) — name/role/is_active with self-protection: you cannot demote or deactivate your own account, so an org can never brick its last admin. Cross-org ids are 404 (RLS + explicit filter).EmailStrneeds email-validator (not shipped) → plain-str shape check;issue_password_tokenmust be imported relatively (.auth) since the package isapp.apiin the container.Frontend —
/system/usersgains an Invite dialog (email/name/role) and, in the user details dialog, a role select + activate/deactivate button on the real PATCH; backend errors (409, self-protection 400s) surface verbatim.Plumbing — compose lims env passes through
SMTP_*+APP_BASE_URL(unset = honest log-only dev mode); k8s lims Deployment reads them from the optionalsmtp-credentialssecret (template + runbook decision row added). The platform deploys and runs without email configured — it logs instead of sending.Proof
tscclean, smoke 4/4,docker compose configvalid, both overlays render🤖 Generated with Claude Code