user-management¶
Purpose¶
user-management owns user identity, organisational membership, role assignments, and permissions. It provides the resolved user context that other services use for authorisation decisions, and exposes admin APIs for creating and managing users, roles, and org memberships. It emits events when membership or role state changes so downstream services can respond without polling.
User identity is backed by AWS Cognito via the CognitoModule — AdminCreateUser / disable / enable / reset go through the SDK. The module accepts a COGNITO_PROVIDER=mock|cognito switch; the default in .env.example is mock so local dev doesn't need an AWS pool, but the mock skips real Cognito calls (no invitation email). To wire a real pool, follow runbooks/cognito-setup.md.
Key endpoints¶
GET /v1/user-management/users/me — fetch the authenticated user's own profile
PATCH /v1/user-management/users/me — update the authenticated user's own profile
GET /v1/user-management/users/:id/context — fetch a user's resolved context (roles, org memberships, product access)
GET /v1/user-management/users/by-external-id/:externalId — look up a user by external identity provider ID
GET /v1/user-management/users/:id/contact — fetch contact details for a user
POST /v1/user-management/admin/users — create a user
GET /v1/user-management/admin/users — list users (paginated; see below)
GET /v1/user-management/admin/users/:id — fetch a user (admin view)
PATCH /v1/user-management/admin/users/:id — update a user (display name, status, and practitioner credentials)
DELETE /v1/user-management/admin/users/:id — deactivate a user
POST /v1/user-management/admin/users/:id/reset-password — trigger a password reset
POST /v1/user-management/admin/users/:userId/memberships — add a user to an organisation
PATCH /v1/user-management/admin/users/:userId/memberships/:membershipId — update a membership
DELETE /v1/user-management/admin/users/:userId/memberships/:membershipId — remove a membership (400 if it would be the user's last)
POST /v1/user-management/admin/users/:userId/memberships/:membershipId/products — grant product access
DELETE /v1/user-management/admin/users/:userId/memberships/:membershipId/products/:productId — revoke product access
POST /v1/user-management/admin/roles — create a role
GET /v1/user-management/admin/roles — list roles
GET /v1/user-management/admin/roles/:id — fetch role with permissions and member_count
GET /v1/user-management/admin/products/:id/grant-count — count of memberships granted this product
PATCH /v1/user-management/admin/roles/:id — update a role
DELETE /v1/user-management/admin/roles/:id — delete a role
POST /v1/user-management/permissions/register — register a permission string
GET /v1/user-management/permissions — list registered permissions
Baseline permissions seeded on boot¶
PermissionsSeeder runs OnModuleInit and upserts ~25 baseline permissions (admin / cases / patients / images / histology / medications / events / webhooks / derm_review / orchestrator / question_sets variants) across the platform, clinical-api, and orchestrator service ids. Idempotent — uses the same upsert-by-permission helper the register endpoint uses, so re-running on every boot is safe and picks up description edits. Operators can still author additional permissions by hitting POST /v1/user-management/permissions/register directly.
This is what fills the Create-Role page's Permissions multi-select on admin-ui; before the seeder existed, the dropdown was empty because nothing populated the RegisteredPermission table.
GET /v1/user-management/admin/users query params + response (Phase 3b)¶
Query params:
organisation_id(optional) — restrict to users with at least one membership in that orguser_type(optional) —clinician|admin|patient|user. The genericusertype carries no profile and gets capabilities purely via role membership.status(optional) —active|suspended|deactivatedsearch(optional) —LIKE %term%overemailanddisplay_name(case-insensitivity comes from MySQL's defaultutf8mb4collation)product_id(optional) — restrict to users with at least one membership granted access to that product. Combined withorganisation_idin the samememberships.someclause, so a hit means a single membership row has both the org and the product grant (not unrelated grants in other orgs).limit(default 25, clamped 1..100)offset(default 0, clamped >= 0)
Response envelope:
{ "items": [...], "total": 1234, "limit": 25, "offset": 50 }
total is the unfiltered-by-pagination count; the front end uses it to compute total pages.
Database tables¶
User — core user record (external ID, active status)
PractitionerProfile — practitioner-specific fields for clinical users
PatientAccount — patient-specific account fields, including clinicalPatientId linking back to a clinical-api.Patient row when the platform user is the result of an admin-initiated promotion from a clinical record
OrgMembership — links a user to an organisation with a role
Role — role definitions with associated permissions
RolePermission — join between roles and registered permissions
RegisteredPermission — platform-wide permission registry
ProductAccess — grants a membership access to a specific product
Events¶
users.deactivated — emit — published to users.events channel when a user is deactivated
users.role.changed — emit — published to users.events channel when a user's role changes
users.permissions.changed — emit — published to users.events channel when a role's permissions change
Dependencies¶
- Redis — event publishing to
users.eventschannel via@sa-platform/commonRedisService - MySQL — primary store, accessed via Prisma 7 driver-adapter pattern
- auth — JWT verification via
@sa-platform/auth-client
Where to learn more¶
- Design spec
- Source:
services/user-management/(in this repo)