Users
The users module provides a light CRUD for user profiles (read + update), endpoints for reading role and permissions, and avatar upload/delete. GET /users/:user_id is polymorphic: if user_id is omitted the server returns the currently logged-in user’s data; the response shape also differs between web and mobile clients.
| Property | Value |
|---|---|
| Base URL | {HOST}/v1 |
| Auth | Bearer JWT (header Authorization) or cookie access_token |
| Content-Type | application/json · avatar upload: multipart/form-data |
| Error envelope | { "message": string | string[], "statusCode": number, "error": string } |
| Validation | Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 |
| Related modules | authentication, organizations, acl, file-manager |
| Document version | v1 · 2026-05-20 |
| Audience | Internal FE devs (mobile + web) |
Summary
Section titled “Summary”FE typically calls GET /users/:user_id without user_id after login to fetch the active user’s profile. GET /users is used by admins to list organization members (or, for the MORIA role, across organizations via org_id). Avatar updates have their own path (POST/DELETE /users/me/profile-image) that writes directly to S3 — profile_image cannot be modified via PATCH /users/:user_id.
| Method | Path | Summary |
|---|---|---|
| GET | /v1/users | List users (org members / cross-org for MORIA) |
| GET | /v1/users/monthly-income-range | Reference data for monthly income ranges |
| GET | /v1/users/:user_id | User detail (or self if param omitted) |
| PATCH | /v1/users/:user_id | Update user profile |
| GET | /v1/users/:user_id/role | Fetch the user’s role (without permissions) |
| GET | /v1/users/:user_id/role/permissions | Fetch the permissions the user holds |
| POST | /v1/users/me/profile-image | Upload/replace profile picture (multipart) |
| DELETE | /v1/users/me/profile-image | Remove profile picture |
GET /v1/users bearer
Section titled “GET /v1/users ”List users (organization members, or cross-organization for the MORIA role). Standard paginated response. The result shape depends on X-Client-Type (web/mobile).
bearer MORIA, ORGANIZATION, INDIVIDUAL read-user RESOURCE_FETCHEDWeb / mobile — honors
x-client-type(defaultmobile).web→ formatted user list (formatUserList());mobile→ rawUsersentity rows.
Query params
Section titled “Query params”org_id is required in practice — the live list is always fetched as GET /v1/users?org_id=<uuid>. MORIA callers must supply it; for other roles it defaults to (and may only target) the caller’s own organization.
| Param | Type | Default | Notes |
|---|---|---|---|
org_id | UUID | — | Required. MORIA must supply it; other roles default to (and may only target) their own organization. |
page | number | 1 | Page number |
limit | number | 10 | Records per page |
order | 'asc' | 'desc' | desc | Order by created_at |
user_status | enum UserStatus | optional | active, inactive, suspended |
user_type | enum UserType | optional | moria, organization, individual |
Response — mobile (default)
Section titled “Response — mobile (default)”Raw Users entity rows: flat audit columns (created_at/updated_at/created_by/…), FK ids (organization_id, role_id, address_id, security_id, settings_id), slug fields, and the full set of KYC columns. The eager organization relation is the raw entity; password and role are not serialized.
{ "status": "success", "statusCode": 200, "message": "Users fetched successfully", "data": { "limit": 10, "count": 4, "currentPage": 1, "totalPages": 1, "users": [ { "id": "05b22417-4e54-4469-8a32-dcde900df999", "created_at": "2026-05-18T09:14:46.566Z", "updated_at": "2026-05-18T09:14:46.566Z", "created_by": "00000000-0000-0000-0000-000000000000", "updated_by": "00000000-0000-0000-0000-000000000000", "deleted_by": "00000000-0000-0000-0000-000000000000", "security_id": null, "settings_id": null, "first_name": "Admin", "last_name": "Org", "middle_name": null, "slug_first_name": "admin", "slug_last_name": "org", "slug_middle_name": null, "id_card_number": null, "education": null, "mother_name": null, "relatives": null, "phone_number": "+6288802760733", "purpose": null, "source_of_income": null, "monthly_income": null, "gender": "male", "date_of_birth": null, "place_of_birth": null, "religion": null, "marital_status": null, "organization_id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "role_id": "efebca8a-2169-478b-b7d6-5c71306d7953", "user_type": "organization", "user_status": "active", "address_id": null, "profile_image": null, "organization": { "id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "created_at": "2026-05-18T08:04:07.185Z", "updated_at": "2026-05-18T08:04:07.185Z", "created_by": "00000000-0000-0000-0000-000000000000", "updated_by": "00000000-0000-0000-0000-000000000000", "deleted_by": "00000000-0000-0000-0000-000000000000", "name": "moria fund", "slug_name": "no_organization_name", "phone_number": "+1234567890", "subdomain": null, "website": null, "official_registration_number": null, "logo_id": null, "organization_type": "moria", "status": "active", "size_id": "dcb3bcb7-7009-4e25-80b0-b8f07d79954b", "industry_id": "626e7efa-06b3-4ee5-a8bf-bcabb409a0db", "address_id": "00000000-0000-0000-0000-000000000000", "settings_id": "00000000-0000-0000-0000-000000000000" }, "address": null } ] }, "lang": "en"}Response — web (x-client-type: web)
Section titled “Response — web (x-client-type: web)”Formatted by formatUserList(): audit columns are nested into created/updated/deleted ({ at, by }), FK ids and slug fields are dropped, and organization/role/accounts/address are reduced to their display fields.
{ "status": "success", "statusCode": 200, "message": "Users fetched successfully", "data": { "limit": 10, "count": 4, "currentPage": 1, "totalPages": 1, "users": [ { "id": "05b22417-4e54-4469-8a32-dcde900df999", "created": { "at": "2026-05-18T09:14:46.566Z", "by": {} }, "updated": { "at": "2026-05-18T09:14:46.566Z", "by": {} }, "deleted": { "at": null, "by": {} }, "first_name": "Admin", "last_name": "Org", "middle_name": null, "id_card_number": null, "education": null, "phone_number": "+6288802760733", "source_of_income": null, "monthly_income": null, "gender": "male", "date_of_birth": null, "place_of_birth": null, "religion": null, "marital_status": null, "user_type": "organization", "user_status": "active", "profile_image": null, "organization": { "id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "name": "moria fund", "phone_number": "+1234567890", "logo_id": null }, "role": { "name": "organization_super_admin", "display_name": "Organization Super Admin", "role_type": "default" }, "accounts": [], "address": null } ] }, "lang": "en"}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | MORIA calls without org_id |
401 Unauthorized | Attempted to view another organization (non-MORIA) |
403 Forbidden | Permission mismatch |
404 Not Found | Organization not found |
GET /v1/users/monthly-income-range bearer
Section titled “GET /v1/users/monthly-income-range ”Reference data for monthly income ranges for profile/KYC dropdowns. Standard pagination.
bearer MORIA, ORGANIZATION, INDIVIDUAL read-userQuery params
Section titled “Query params”| Param | Type | Default | Notes |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 10 | Records per page |
order | 'asc' | 'desc' | desc | Order by created_at |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "Monthly income ranges retrieved successfully", "data": { "limit": 10, "count": 6, "currentPage": 1, "totalPages": 1, "ranges": [ { "id": "...", "label": "0 – 5.000.000", "min": 0, "max": 5000000 } ] }}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
401 Unauthorized | Invalid Bearer/cookie |
403 Forbidden | Permission mismatch |
GET /v1/users/:user_id bearer
Section titled “GET /v1/users/:user_id ”User detail. If user_id is omitted the server returns the logged-in user’s data. The result shape depends on the caller’s UserType and the X-Client-Type header.
bearer read-user RESOURCE_FETCHEDWeb / mobile — honors
x-client-type(defaultmobile).web→ formatted detail (formatUserDetail());mobile→ raw entity withtwo_factor_authentication_secret,role, andpasswordremoved.
Path params
Section titled “Path params”| Param | Type | Notes |
|---|---|---|
user_id | UUID | Optional. If omitted → active user (self). |
Response — mobile (default)
Section titled “Response — mobile (default)”Raw Users entity (same fields as one item in the list above): flat audit columns, FK ids, slug fields, and the full KYC column set. two_factor_authentication_secret, role, and password are not serialized.
{ "status": "success", "statusCode": 200, "message": "User data fetched successfully", "data": { "user": { "id": "05b22417-4e54-4469-8a32-dcde900df999", "created_at": "2026-05-18T09:14:46.566Z", "updated_at": "2026-05-18T09:14:46.566Z", "created_by": "00000000-0000-0000-0000-000000000000", "updated_by": "00000000-0000-0000-0000-000000000000", "deleted_by": "00000000-0000-0000-0000-000000000000", "security_id": null, "settings_id": null, "first_name": "Admin", "last_name": "Org", "middle_name": null, "slug_first_name": "admin", "slug_last_name": "org", "slug_middle_name": null, "id_card_number": null, "education": null, "mother_name": null, "relatives": null, "phone_number": "+6288802760733", "purpose": null, "source_of_income": null, "monthly_income": null, "gender": "male", "date_of_birth": null, "place_of_birth": null, "religion": null, "marital_status": null, "organization_id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "role_id": "efebca8a-2169-478b-b7d6-5c71306d7953", "user_type": "organization", "user_status": "active", "address_id": null, "profile_image": null, "organization": { "id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "created_at": "2026-05-18T08:04:07.185Z", "updated_at": "2026-05-18T08:04:07.185Z", "created_by": "00000000-0000-0000-0000-000000000000", "updated_by": "00000000-0000-0000-0000-000000000000", "deleted_by": "00000000-0000-0000-0000-000000000000", "name": "moria fund", "slug_name": "no_organization_name", "phone_number": "+1234567890", "subdomain": null, "website": null, "official_registration_number": null, "logo_id": null, "organization_type": "moria", "status": "active", "size_id": "dcb3bcb7-7009-4e25-80b0-b8f07d79954b", "industry_id": "626e7efa-06b3-4ee5-a8bf-bcabb409a0db", "address_id": "00000000-0000-0000-0000-000000000000", "settings_id": "00000000-0000-0000-0000-000000000000" }, "address": null } }, "lang": "en"}Response — web (x-client-type: web)
Section titled “Response — web (x-client-type: web)”Formatted by formatUserDetail(): audit columns are nested into created/updated/deleted ({ at, by }), FK ids and slug fields are dropped, and organization/role/accounts/address are reduced to their display fields (same per-user shape as the web list rows above).
{ "status": "success", "statusCode": 200, "message": "User data fetched successfully", "data": { "user": { "id": "05b22417-4e54-4469-8a32-dcde900df999", "created": { "at": "2026-05-18T09:14:46.566Z", "by": {} }, "updated": { "at": "2026-05-18T09:14:46.566Z", "by": {} }, "deleted": { "at": null, "by": {} }, "first_name": "Admin", "last_name": "Org", "middle_name": null, "id_card_number": null, "education": null, "phone_number": "+6288802760733", "source_of_income": null, "monthly_income": null, "gender": "male", "date_of_birth": null, "place_of_birth": null, "religion": null, "marital_status": null, "user_type": "organization", "user_status": "active", "profile_image": null, "organization": { "id": "2fd30e18-b135-4c31-9967-32f684bb58d8", "name": "moria fund", "phone_number": "+1234567890", "logo_id": null }, "role": { "name": "organization_super_admin", "display_name": "Organization Super Admin", "role_type": "default" }, "accounts": [], "address": null } }, "lang": "en"}A non-UUID user_id is rejected before formatting: { "message": "Invalid UUID", "error": "Bad Request", "statusCode": 400, "status": "error", "lang": "en" }.
Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | user_id is not a UUID (if sent) |
401 Unauthorized | Attempted to access a user from another organization |
403 Forbidden | Permission mismatch |
404 Not Found | User not found |
PATCH /v1/users/:user_id bearer
Section titled “PATCH /v1/users/:user_id ”Update a user profile. Only for users in the same organization. All fields are optional — send only what changes. The profile_image field is intentionally absent from the DTO.
Path params
Section titled “Path params”| Param | Type | Notes |
|---|---|---|
user_id | UUID | ID of the user being updated |
Request body — UpdateUserDto (all fields optional)
Section titled “Request body — UpdateUserDto (all fields optional)”| Field | Type | Notes |
|---|---|---|
first_name, last_name, middle_name | string | Basic identity |
phone_number | string | International format (+62…) |
id_card_number | string | KTP number (16 digits) |
education | enum Education | primary_school, junior_high, senior_high, diploma, bachelor, postgraduate, other |
mother_name, relatives | string | Additional KYC data |
purpose, source_of_income, montly_income | string | Financial profile (typo montly_income retained) |
gender | enum Gender | male, female |
date_of_birth | date | ISO 8601 (YYYY-MM-DD) |
place_of_birth | string | City of birth |
religion | enum Religion | islam, christianity, hinduism, buddhism, confucianism, other |
marital_status | enum MaritalStatus | single, married, divorced, widowed |
province, city, country, district, subdistrict, village | string | Address |
rt, rw, postal_code | string | Address detail |
address_type | enum AddressType | Default INDIVIDUAL |
Example request
Section titled “Example request”{ "first_name": "Aisha", "phone_number": "+628156489102", "marital_status": "married", "city": "Bandung"}Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "User updated successfully", "data": { "user": { "id": "770e8400-e29b-41d4-a716-446655440222", "first_name": "Aisha", "phone_number": "+628156489102", "marital_status": "married", "updated_at": "2026-05-20T10:00:00.000Z" } }}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | Validation failed (invalid enum, bad date format) |
401 Unauthorized | Updating a user from another organization |
403 Forbidden | Missing update-user permission |
404 Not Found | User not found |
GET /v1/users/:user_id/role bearer
Section titled “GET /v1/users/:user_id/role ”Fetch the role attached to a user (without permissions and role_type). If the user has no role yet, the response is successful with no data field.
Path params
Section titled “Path params”| Param | Type | Notes |
|---|---|---|
user_id | UUID | Target user ID |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "user role fetched successfully", "data": { "role": { "id": "03be5259-f281-478e-a8d0-e7e825e525f2", "name": "organization_admin", "description": "Admin role for the organization" } }}If the user has no role: 200 response with message: "No role assigned to this user" and no data field.
Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
403 Forbidden | Permission mismatch |
404 Not Found | User not found |
GET /v1/users/:user_id/role/permissions bearer
Section titled “GET /v1/users/:user_id/role/permissions ”Fetch all permissions the user holds via their role. If they have no role, the response is successful with no data field.
Path params
Section titled “Path params”| Param | Type | Notes |
|---|---|---|
user_id | string | User ID (no UUID pipe at the controller — still send a valid UUID) |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "user role permissions fetched successfully", "data": { "permissions": [ { "id": "...", "name": "read-user" }, { "id": "...", "name": "update-user" } ] }}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
403 Forbidden | Permission mismatch |
404 Not Found | User not found |
POST /v1/users/me/profile-image bearer
Section titled “POST /v1/users/me/profile-image ”Upload or replace the active user’s profile picture. The backend accepts a multipart file (max 5 MB, JPEG/PNG/WebP), uploads to S3 (public-read), removes the old object, and stores the key in the users.profile_image column.
Request body — multipart/form-data
Section titled “Request body — multipart/form-data”| Field | Type | Required | Notes |
|---|---|---|---|
file | binary | yes | JPEG / PNG / WebP, maximum size 5 MB |
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "profile image updated successfully", "data": { "profile_image": "users/770e8400.../avatar-1716200000.jpg" }}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
400 Bad Request | Invalid file (empty, unsupported MIME, size > 5 MB) |
401 Unauthorized | Invalid Bearer/cookie |
403 Forbidden | Missing update-user permission |
Side effects
Section titled “Side effects”- Upload to S3 (public-read bucket) and remove the old object (if any).
- Update the
users.profile_imagecolumn with the new key. - Emit
BusinessEventRESOURCE_UPDATED(impactLOW).
DELETE /v1/users/me/profile-image bearer
Section titled “DELETE /v1/users/me/profile-image ”Delete the active user’s profile picture. The backend removes the S3 object referenced by users.profile_image and clears that column.
Response — 200 OK
Section titled “Response — 200 OK”{ "status": "success", "statusCode": 200, "message": "profile image removed successfully", "data": {}}Errors
Section titled “Errors”| Status | When it occurs |
|---|---|
401 Unauthorized | Invalid Bearer/cookie |
403 Forbidden | Missing update-user permission |
Side effects
Section titled “Side effects”- Remove S3 object (if any).
- Set
users.profile_image = NULL.
Reference
Section titled “Reference”Enum: UserStatus
Section titled “Enum: UserStatus”active,inactive,suspended
Enum: UserType
Section titled “Enum: UserType”moria,organization,individual
Enum: Education
Section titled “Enum: Education”primary_school,junior_high,senior_highdiploma,bachelor,postgraduate,other
Enum: Religion
Section titled “Enum: Religion”islam,christianity,hinduismbuddhism,confucianism,other
Enum: MaritalStatus
Section titled “Enum: MaritalStatus”single,married,divorced,widowed
Standard error envelope
Section titled “Standard error envelope”{ "message": "you can't view another organization", "statusCode": 401, "error": "Unauthorized"}message can be a string or an array of strings (multi-field validation errors).
Client-type header
Section titled “Client-type header”X-Client-Type: web— “formatted” response shapeX-Client-Type: mobile— lean shape (norole,password,security)
Common HTTP codes
Section titled “Common HTTP codes”400body/query/param validation401cross-organization access403permission mismatch404user not found