Skip to content

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 CognitoModuleAdminCreateUser / 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 org
  • user_type (optional) — clinician | admin | patient | user. The generic user type carries no profile and gets capabilities purely via role membership.
  • status (optional) — active | suspended | deactivated
  • search (optional) — LIKE %term% over email and display_name (case-insensitivity comes from MySQL's default utf8mb4 collation)
  • product_id (optional) — restrict to users with at least one membership granted access to that product. Combined with organisation_id in the same memberships.some clause, 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.events channel via @sa-platform/common RedisService
  • 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)