Skip to content

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.

PropertyValue
Base URL{HOST}/v1
AuthBearer JWT (header Authorization) or cookie access_token
Content-Typeapplication/json · avatar upload: multipart/form-data
Error envelope{ "message": string | string[], "statusCode": number, "error": string }
ValidationGlobal ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400
Related modulesauthentication, organizations, acl, file-manager
Document versionv1 · 2026-05-20
AudienceInternal FE devs (mobile + web)

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.

MethodPathSummary
GET/v1/usersList users (org members / cross-org for MORIA)
GET/v1/users/monthly-income-rangeReference data for monthly income ranges
GET/v1/users/:user_idUser detail (or self if param omitted)
PATCH/v1/users/:user_idUpdate user profile
GET/v1/users/:user_id/roleFetch the user’s role (without permissions)
GET/v1/users/:user_id/role/permissionsFetch the permissions the user holds
POST/v1/users/me/profile-imageUpload/replace profile picture (multipart)
DELETE/v1/users/me/profile-imageRemove profile picture

List users (organization members, or cross-organization for the MORIA role). Standard paginated response. The result shape depends on X-Client-Type (web/mobile).

Web / mobile — honors x-client-type (default mobile). web → formatted user list (formatUserList()); mobile → raw Users entity rows.

bearer MORIA, ORGANIZATION, INDIVIDUAL read-user RESOURCE_FETCHED

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.

ParamTypeDefaultNotes
org_idUUIDRequired. MORIA must supply it; other roles default to (and may only target) their own organization.
pagenumber1Page number
limitnumber10Records per page
order'asc' | 'desc'descOrder by created_at
user_statusenum UserStatusoptionalactive, inactive, suspended
user_typeenum UserTypeoptionalmoria, organization, individual

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,
"email": "[email protected]",
"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",
"email": "[email protected]",
"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"
}

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": {}
},
"email": "[email protected]",
"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",
"email": "[email protected]",
"phone_number": "+1234567890",
"logo_id": null
},
"role": {
"name": "organization_super_admin",
"display_name": "Organization Super Admin",
"role_type": "default"
},
"accounts": [],
"address": null
}
]
},
"lang": "en"
}
StatusWhen it occurs
400 Bad RequestMORIA calls without org_id
401 UnauthorizedAttempted to view another organization (non-MORIA)
403 ForbiddenPermission mismatch
404 Not FoundOrganization 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-user
ParamTypeDefaultNotes
pagenumber1Page number
limitnumber10Records per page
order'asc' | 'desc'descOrder by created_at
{
"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 }
]
}
}
StatusWhen it occurs
401 UnauthorizedInvalid Bearer/cookie
403 ForbiddenPermission mismatch

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.

Web / mobile — honors x-client-type (default mobile). web → formatted detail (formatUserDetail()); mobile → raw entity with two_factor_authentication_secret, role, and password removed.

bearer read-user RESOURCE_FETCHED
ParamTypeNotes
user_idUUIDOptional. If omitted → active user (self).

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,
"email": "[email protected]",
"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",
"email": "[email protected]",
"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"
}

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": {} },
"email": "[email protected]",
"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",
"email": "[email protected]",
"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" }.

StatusWhen it occurs
400 Bad Requestuser_id is not a UUID (if sent)
401 UnauthorizedAttempted to access a user from another organization
403 ForbiddenPermission mismatch
404 Not FoundUser 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.

bearer update-user RESOURCE_UPDATED
ParamTypeNotes
user_idUUIDID of the user being updated

Request body — UpdateUserDto (all fields optional)

Section titled “Request body — UpdateUserDto (all fields optional)”
FieldTypeNotes
first_name, last_name, middle_namestringBasic identity
phone_numberstringInternational format (+62…)
id_card_numberstringKTP number (16 digits)
educationenum Educationprimary_school, junior_high, senior_high, diploma, bachelor, postgraduate, other
mother_name, relativesstringAdditional KYC data
purpose, source_of_income, montly_incomestringFinancial profile (typo montly_income retained)
genderenum Gendermale, female
date_of_birthdateISO 8601 (YYYY-MM-DD)
place_of_birthstringCity of birth
religionenum Religionislam, christianity, hinduism, buddhism, confucianism, other
marital_statusenum MaritalStatussingle, married, divorced, widowed
province, city, country, district, subdistrict, villagestringAddress
rt, rw, postal_codestringAddress detail
address_typeenum AddressTypeDefault INDIVIDUAL
{
"first_name": "Aisha",
"phone_number": "+628156489102",
"marital_status": "married",
"city": "Bandung"
}
{
"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"
}
}
}
StatusWhen it occurs
400 Bad RequestValidation failed (invalid enum, bad date format)
401 UnauthorizedUpdating a user from another organization
403 ForbiddenMissing update-user permission
404 Not FoundUser 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.

bearer read-role RESOURCE_FETCHED
ParamTypeNotes
user_idUUIDTarget user ID
{
"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.

StatusWhen it occurs
403 ForbiddenPermission mismatch
404 Not FoundUser 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.

bearer read-permission RESOURCE_FETCHED
ParamTypeNotes
user_idstringUser ID (no UUID pipe at the controller — still send a valid UUID)
{
"status": "success",
"statusCode": 200,
"message": "user role permissions fetched successfully",
"data": {
"permissions": [
{ "id": "...", "name": "read-user" },
{ "id": "...", "name": "update-user" }
]
}
}
StatusWhen it occurs
403 ForbiddenPermission mismatch
404 Not FoundUser 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.

bearer update-user RESOURCE_UPDATED
FieldTypeRequiredNotes
filebinaryyesJPEG / PNG / WebP, maximum size 5 MB
{
"status": "success",
"statusCode": 200,
"message": "profile image updated successfully",
"data": {
"profile_image": "users/770e8400.../avatar-1716200000.jpg"
}
}
StatusWhen it occurs
400 Bad RequestInvalid file (empty, unsupported MIME, size > 5 MB)
401 UnauthorizedInvalid Bearer/cookie
403 ForbiddenMissing update-user permission
  • Upload to S3 (public-read bucket) and remove the old object (if any).
  • Update the users.profile_image column with the new key.
  • Emit BusinessEvent RESOURCE_UPDATED (impact LOW).

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.

bearer update-user RESOURCE_DELETED
{
"status": "success",
"statusCode": 200,
"message": "profile image removed successfully",
"data": {}
}
StatusWhen it occurs
401 UnauthorizedInvalid Bearer/cookie
403 ForbiddenMissing update-user permission
  • Remove S3 object (if any).
  • Set users.profile_image = NULL.

  • active, inactive, suspended
  • moria, organization, individual
  • primary_school, junior_high, senior_high
  • diploma, bachelor, postgraduate, other
  • islam, christianity, hinduism
  • buddhism, confucianism, other
  • single, married, divorced, widowed
{
"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).

  • X-Client-Type: web — “formatted” response shape
  • X-Client-Type: mobile — lean shape (no role, password, security)
  • 400 body/query/param validation
  • 401 cross-organization access
  • 403 permission mismatch
  • 404 user not found