---
module: msp.users
title: Users
tab: Claude
order: 40
audience: dev
---

Act-without-guessing reference. Everything below is confirmed in code.

## Ownership
- **User records owner:** `hiveops-auth` — `MspUserController` (port 8082, NGINX `/api/auth/`, DB `hiveiq_auth`).
- **Institution list + scope resolver:** `hiveops-mgmt` — `MspController` + `CustomerInternalController` (port 8080, DB `hiveiq_mgmt`).
- Frontend: `hiveops-msp/frontend/src/components/Users/UsersView.svelte` (app `msp`, `msp.bcos.cloud`, port 5186). No dedicated backend for the SPA.

## Endpoints (METHOD path)
| Method + path | Service | Auth |
|---|---|---|
| GET `/auth/api/msp/users?page=&size=&search=&role=&status=&institutionKey=` | auth | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| PUT `/auth/api/msp/users/{id}` | auth | class role + `isOwned` |
| POST `/auth/api/msp/users/{id}/enable` | auth | class role + `isOwned` |
| POST `/auth/api/msp/users/{id}/disable` | auth | class role + `isOwned` + self-disable guard |
| POST `/auth/api/msp/users/{id}/reset-password` | auth | class role + `isOwned` |
| GET `/api/v1/msp/institutions` (SPA calls `/mgmt/api/v1/msp/institutions`) | mgmt | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| GET `/api/internal/customers/domain/{domain}/institution-keys` | mgmt | header `X-Internal-Secret` (NOT user JWT) |

- `{id}` = numeric `users.id` (DB `hiveiq_auth`).
- PUT body: `{name, role, fleetTaskApprover}` — `fleetTaskApprover` is a **string** `"true"`/`"false"`. `role` must be a valid `User.Role` name.
- List response shape: `{content:[user...], page:{totalElements,totalPages,number,size}}`. SPA reads `res.data.content` only.
- SPA always requests `size=200` and filters/groups client-side; `page`/`search`/`role`/`status` params exist server-side but the SPA doesn't use them.

## Roles
- All `MspUserController` endpoints: class-level `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`.
- `BCOS_ADMIN` = superuser (all institutions, bypasses scoping). `MSP_ADMIN` = scoped to own institutions.
- `mgmt` MspController endpoints: `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.
- Internal keys endpoint: service secret only (`X-Internal-Secret` = auth env `internal.service-secret` / mgmt side).

## Tables / DB
- **`users`** (DB `hiveiq_auth`, entity `User`) — read/written. `SYSTEM` role always excluded from list.
- **`customer_domains`** (DB `hiveiq_auth`) — email-domain→institutionKey; drives scope fallback + response `institutionKey`/`institution`.
- **`password_reset_tokens`** (DB `hiveiq_auth`) — written by reset-password.
- mgmt (DB `hiveiq_mgmt`): `customer_institution_access`, `customers`, `licenses`.

## Kafka
- NONE. Fully synchronous HTTP (auth ↔ mgmt). No topics produced or consumed for this feature.

## Scope resolution (exact)
`auth MspUserController.resolveInstitutionKeys` → JWT email domain → `MgmtServiceClient.getInstitutionKeys` → mgmt `GET /api/internal/customers/domain/{domain}/institution-keys` (X-Internal-Secret). Empty/failure → caller sees empty list. Ownership (`isOwned`): `user.customer_license_key ∈ keys` OR email-domain→`customer_domains`.institutionKey ∈ keys.

## Gotchas
- `enable` also clears `failed_login_attempts=0` + `locked_until=null` → it doubles as **unlock**.
- `disable` self-disable → HTTP **400** (`"You cannot disable your own account"`).
- `reset-password` always returns **200** even if email send fails (caught+logged WARN); does NOT set a password, only mails a token.
- Invalid `role` in PUT → silently ignored (`Role.valueOf` in try/catch), other fields still saved.
- Email is immutable via this controller — no change-email path.
- Role/status server-side filter is applied **after** pagination → `page.totalElements/totalPages` are pre-filter and inconsistent; SPA avoids this by client-side filtering `size=200`.
- SPA shows max 200 users (hard-coded `size=200`, no pagination UI).
- `customer_license_key IN keys` clause rarely matches (license keys ≠ institution keys); scoping effectively rides `customer_domains`.
- PUT accepts ANY valid `User.Role` name (e.g. `BCOS_ADMIN`, `ADMIN`) — no allowlist; SPA only offers CUSTOMER/MSP_ADMIN. (see Do NOT)

## Do NOT
- Do NOT look for user records in `hiveiq_mgmt` — users live in `hiveiq_auth`, owned by hiveops-auth.
- Do NOT expect a create-user endpoint here — `MspUserController` has list/edit/enable/disable/reset only (no POST create; mock server's `POST /api/msp/users` is not implemented in the real backend).
- Do NOT call `/api/internal/customers/domain/.../institution-keys` with a user JWT — it needs `X-Internal-Secret`.
- Do NOT trust the JWT institution claim for scope — auth re-resolves from mgmt per request.
- Do NOT expect a Kafka topic — there is none for this feature.
- Do NOT PUT `role` to a privileged value expecting the API to block it — it won't; enforce role allowlists caller-side.
- Do NOT treat a 200 from reset-password as "email delivered."
