Auth Module API & Integration Guide
Endpoint reference for admin auth and customer mobile auth — login by email or mobile number, refresh, sessions, password reset, and verification.
Auth - API & Integration Guide
1. Route Surfaces
Auth is split across two surfaces:
- Admin / backoffice auth:
/api/auth/* - Customer mobile auth:
/api/mobile/auth/*
Accounts are created by an administrator through the Users and Team modules. There is no
self-service registration and no self-service account deletion. banned is the only account
control.
An administrator-created account arrives with no password and an unverified address. The recipient redeems the invitation link, which verifies the address and sets the first password in one step — see Account invitations. There is no separate verification step before or after it.
2. Endpoint Reference
2.1 Admin auth (/api/auth/*)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/login/email | Public | Admin password login by email address or mobile number (§3.1) |
| POST | /api/auth/password/forgot | Public | Request password reset |
| POST | /api/auth/password/reset | Public | Reset password via token or OTP |
| POST | /api/auth/invitation/accept | Public | Redeem an account invitation: verify the address and set the first password |
| POST | /api/auth/password/set | JWT | Set or change password for authenticated user |
| POST | /api/auth/email/verify/resend | Public | Resend email verification |
| POST | /api/auth/email/verify/confirm | Public | Confirm email verification, by link or by { email, otp } |
| POST | /api/auth/email/change | JWT | Request a change of sign-in address |
| POST | /api/auth/email/change/confirm-code | JWT | Confirm a pending email change with the 6-digit code |
| POST | /api/auth/email/change/confirm | Public | Confirm a pending email change with the link token |
| POST | /api/auth/email/change/cancel | Public | Cancel a pending email change from the old address's link |
| POST | /api/auth/refresh | Public | Rotate refresh/access tokens (§3.1a) |
| GET | /api/auth/me | JWT | Get current user profile (includes hasPassword) |
| GET | /api/auth/permissions | JWT | Get the active role's permission codes; a superadmin receives the catalogue minus the person-bound screen codes (§3.5) |
| POST | /api/auth/phone/verify/request | JWT | Send phone verification OTP |
| POST | /api/auth/phone/verify/confirm | JWT | Confirm phone verification OTP |
| DELETE | /api/auth/logout | JWT | Logout current session |
2.2 Customer mobile auth (/api/mobile/auth/*)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/mobile/auth/login/email | Public | Customer password login by email address or mobile number (§3.1) |
| POST | /api/mobile/auth/refresh | Public | Rotate refresh/access tokens (§3.1a) |
| POST | /api/mobile/auth/password/forgot | Public | Request password reset |
| POST | /api/mobile/auth/password/reset | Public | Reset password via token or OTP |
| POST | /api/mobile/auth/password/set | JWT | Set password for authenticated mobile user |
| POST | /api/mobile/auth/email/verify/resend | Public | Resend email verification |
| POST | /api/mobile/auth/email/verify/confirm | Public | Confirm email verification, by link or by { email, otp } |
| POST | /api/mobile/auth/email/change | JWT | Request a change of sign-in address |
| POST | /api/mobile/auth/email/change/confirm-code | JWT | Confirm a pending email change with the 6-digit code |
| POST | /api/mobile/auth/email/change/confirm | Public | Confirm a pending email change with the link token |
| POST | /api/mobile/auth/email/change/cancel | Public | Cancel a pending email change from the old address's link |
| GET | /api/mobile/auth/me | JWT | Get current user profile (includes hasPassword) |
| DELETE | /api/mobile/auth/logout | JWT | Logout current session |
Both mounts share the same AuthEmailChangeService and AuthService, so a route
behaves identically on either prefix; /api/mobile/auth/* exists so the mobile
Swagger document carries these operations under its own tag rather than the admin
one.
3. Key Contracts
3.1 Login by email address or mobile number
POST /api/auth/login/email and POST /api/mobile/auth/login/email. The path keeps
its name for compatibility; both routes accept either identifier.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Exactly one of email / identifier | The account's email address. On the mobile route it must be a valid address (@IsEmail); the admin route passes it to the lookup as sent |
identifier | string | Exactly one of email / identifier | "Email address or mobile number". Trimmed, then 3–254 characters. A value containing @ is an email address and behaves exactly like email; anything else is a mobile number |
password | string | Yes | The account's password. 8–32 characters on the mobile route |
deviceInfo | object | Mobile route only | { deviceId, deviceType, deviceName? }; deviceType android or ios registers the device and makes the session a mobile session |
Email, as every existing client sends it:
{
"email": "customer@example.com",
"password": "StrongPass123",
"deviceInfo": {
"deviceId": "android-abc",
"deviceType": "android",
"deviceName": "Pixel"
}
}Mobile number (the admin route takes the same body without deviceInfo):
{
"identifier": "+977 980-0100001",
"password": "StrongPass123",
"deviceInfo": {
"deviceId": "android-abc",
"deviceType": "android"
}
}How a mobile number is read. Spaces, dashes, dots and parentheses are removed; a
leading +977, 00977 or 977 is dropped when exactly ten digits follow, then one
leading 0 when exactly ten digits follow. The result must be exactly ten digits.
9800100001, +977 9800100001, +977-980-0100001, 00977 9800100001 and
09800100001 are the same number, and the number stored on each account is read the
same way. Anything that does not come out as ten digits matches nobody.
Which account signs in. Several accounts may share a number. Of the accounts on the number that could sign in by email — not deleted, not banned, allowed to sign in, email address verified, password set — the one whose password matches signs in. Five is the most a number may carry; more than five is refused before any password is checked.
Response. Identical in shape to an email sign-in — 200 with
{ message: "Login successful.", data: { user, tokens: { sessionId, accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } } }.
The tokens object is described field by field in
§3.1a.
Which role the session starts with. Every permission check reads the session's active role, and the two routes choose it differently:
| Roles the person holds | POST /api/auth/login/email | POST /api/mobile/auth/login/email |
|---|---|---|
| none | no active role | no active role |
| exactly one | that role | that role |
several, teacher among them (for example staff + teacher, guardian + teacher, superadmin + teacher) | no active role | teacher |
several, no teacher | no active role | no active role |
Signing in by email address and by mobile number choose the same way.
data.user.role in the sign-in reply is the name of the role the session starts with
when it starts with one — teacher for a staff + teacher or guardian + teacher
holder on the mobile route. With no active role it is the name of one of the roles the
person holds, in no defined order, or null when they hold none; read
requiresRoleSelection and roles from GET /api/auth/me in that case.
With no active role, GET /api/auth/me reports requiresRoleSelection: true and every
permission-gated route answers 403 AUTH_ACTIVE_ROLE_REQUIRED. With teacher active,
the teacher routes (/api/mobile/teacher/*) need no switch first, and the guardian
routes (/api/mobile/guardian/*) answer 403 PERMISSION_INSUFFICIENT until the
session switches to guardian.
A person who wants another role switches the session with
POST /api/auth/context/switch and body { "roleId": <id> }, taking the id from
roles on GET /api/auth/me. The mobile auth surface has no switch route and lists no
roles — GET /api/mobile/auth/me reports only role, the active role's name — so the
app calls the admin routes for both, with the same bearer token. The switch belongs to
the session: a refresh keeps it, and the next sign-in on the mobile route starts as
teacher again. A roleId the caller does not hold answers 403
AUTH_ACTIVE_ROLE_NOT_HELD.
Errors
| Status | errorCode | When | What the client should do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | Both email and identifier sent, or neither; identifier not a string of 3–254 characters after trimming; mobile route only — email not an address, password outside 8–32, deviceInfo invalid, an unknown field | Fix the request |
| 401 | AUTH_INVALID_CREDENTIALS | Wrong password; unknown address or number; a number that is not ten digits once normalised; an account that may not sign in | Show a generic "incorrect details" message — it deliberately does not say which part was wrong |
| 401 | AUTH_PHONE_LOGIN_AMBIGUOUS | The password matches more than one account on the number, or more than five eligible accounts share it. Message: "Several accounts use this number. Sign in with your email instead." | Ask the person to sign in with their email address |
| 401 | AUTH_EMAIL_VERIFICATION_PENDING | Email path only: the address is not verified yet | Prompt the person to verify their address |
| 401 | AUTH_ACCOUNT_BANNED | Email path only: the account is banned | Show the message; the account cannot sign in |
| 429 | RATE_LIMIT_EXCEEDED | The AUTH_ATTEMPT rate limit: five per caller address and submitted identifier, sixty per address, each per fifteen minutes | Wait and retry |
On the mobile-number path an unverified or banned account is simply not a candidate, so those two codes never come back from it — the attempt answers as if that account were absent. The phone number itself does not need to be verified: any number saved on an account signs in to it with that account's password.
The AUTH_ATTEMPT limit counts a mobile number by its ten digits, not by how it was
typed: 9800100001, +977 9800100001 and 09800100001 share one budget of five. An
email address is counted trimmed and lower-cased. Every attempt by number performs the
same five password checks before deciding, so that work — the bulk of the reply time —
does not depend on how many accounts hold the number.
Password reset (password/forgot, password/reset) stays email-based; there is no
reset by mobile number.
3.1a The tokens object, and refreshing it
Both login routes and both refresh routes return the same tokens object
(AuthTokensDto). Every field is always present.
| Field | Type | Notes |
|---|---|---|
sessionId | string (UUID) | The session's id. A refresh keeps it; send it with every refresh |
accessToken | string | The bearer JWT, sent as Authorization: Bearer <accessToken> |
refreshToken | string | Single use. A refresh returns a new one, and the one sent stops working |
accessTokenExpiresAt | string, ISO-8601 date-time in UTC | When the access token expires: the moment it was issued plus JWT_ACCESS_TOKEN_TTL_SECONDS (900 seconds by default) |
refreshTokenExpiresAt | string, ISO-8601 date-time in UTC | When the refresh token expires: the moment it was issued plus JWT_REFRESH_TOKEN_TTL_SECONDS (604800 seconds, seven days, by default). It is the session's own expiry |
Both timestamps are written with millisecond precision and a Z suffix, for
example 2026-10-08T10:15:00.000Z, and both come from the server's clock.
Refreshing. POST /api/auth/refresh takes refreshToken and sessionId;
POST /api/mobile/auth/refresh takes the same two fields plus deviceInfo. Both
answer 200 with { message: "Tokens refreshed.", data: { user, tokens } }.
A refresh keeps sessionId, issues a new access token and a new refresh token,
and sets the session's expiry to one refresh lifetime after the refresh. The
refreshTokenExpiresAt it returns is that new expiry, so the window slides with
use: a session lapses only after a full refresh lifetime with no refresh.
A tokens object returned by a refresh at 2026-10-08T10:00:00Z, with the default
lifetimes:
{
"sessionId": "0199c3a2-5d1e-7b40-9a61-2f4c8e7d1a90",
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "9f2c4e...",
"accessTokenExpiresAt": "2026-10-08T10:15:00.000Z",
"refreshTokenExpiresAt": "2026-10-15T10:00:00.000Z"
}When to refresh. Refresh shortly before accessTokenExpiresAt rather than
waiting for a 401, leaving a margin for a device clock that differs from the
server's. Send one refresh at a time and keep the refreshToken it returns:
presenting a refresh token a refresh has already replaced ends the session.
Errors
| Status | errorCode | When | What the client should do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | refreshToken or sessionId missing or not a string; an unknown field; on the mobile route, an invalid deviceInfo | Fix the request |
| 401 | AUTH_SESSION_NOT_FOUND | No session has that sessionId — it was logged out, or ended by one of the refusals below | Sign in again |
| 401 | AUTH_SESSION_EXPIRED | The session is past refreshTokenExpiresAt. The session is ended | Sign in again |
| 401 | AUTH_REFRESH_TOKEN_MISMATCH | The refreshToken is not the session's current one. The session is ended | Sign in again |
| 401 | AUTH_SESSION_ACTOR_NOT_FOUND | The session's user no longer exists. The session is ended | Sign in again |
| 401 | AUTH_ACCOUNT_BANNED | The account is banned | Show the message; the account cannot sign in |
| 429 | RATE_LIMIT_EXCEEDED | The AUTH_TOKEN_REFRESH rate limit: ten refreshes a minute | Wait and retry with the same tokens |
3.2 Password reset, and account invitations
POST /api/auth/password/reset and POST /api/mobile/auth/password/reset redeem
an emailed credential and set a password. Send either token or otp, never
neither:
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | One of token/otp | The value from the emailed link |
otp | string | One of token/otp | Exactly six digits |
email | string | With otp, if no phone | Identifies whose code this is |
phone | string | With otp, if no email | E.164; validated on the way in |
password | string | Yes | 8-32 characters, and must satisfy the strong-password rule |
token accepts either account-entry purpose — a password_reset or an
account_invite. Somebody who has been sent a link cannot tell which of the two
they hold, and both mean the same thing from their side: choose a password. An
invitation is valid for 7 days; a reset for the OTP expiry, 15 minutes by default.
An invitation redeemed here also marks the address verified, on the same condition
the dedicated endpoint applies: the record's channel is email and its stored
address still equals the account's. /api/auth/invitation/accept is the endpoint
the invitation email points at, and the one to integrate against; this branch
exists so a link pasted into the reset screen still works.
otp accepts password_reset only. Account invitations are redeemable by
link only. The code is not printed in the invitation email, and the OTP lookup
matches on purpose and person without a token hash to tell two live records apart
— so accepting both purposes there could check a correct code against the wrong
record, fail, and spend one of the five attempts that retire it.
Redeeming either credential retires the person's other account-entry credential, so an invitation and an administrator-issued reset cannot both stay usable once one of them has set a password.
Errors:
400 AUTH_PASSWORD_RESET_INPUT_REQUIRED— neithertokennorotpwas sent400 AUTH_OTP_REQUIRED— the OTP branch was taken with nootp400 AUTH_OTP_TARGET_REQUIRED—otpsent with neitheremailnorphone400 AUTH_VERIFICATION_CODE_INVALID— the token or code is unknown, expired, already redeemed, or was retired by the person redeeming their other link. Ask for a fresh link rather than retrying this one.
Every successful reset notifies the account holder by email, including one they asked for themselves. That notification is the only signal a person gets that somebody else changed their password, so it is not suppressed for the expected case. Delivery failure never rolls back the password change.
Setting or resetting a password anywhere on the account retires any pending email change (§3.3a) as part of the same write, on top of ending every other session.
3.2a Confirming email verification: link or { email, otp }
POST /api/auth/email/verify/confirm and its mobile mount accept either the
link's token, or the account's email plus the 6-digit otp from the same
email — a phone app has no page to open a link on, so it uses the second form;
the panel's link page uses the first.
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | With no email/otp | The value from the verification link |
otp | string | With email, and no token | Exactly six digits |
email | string | With otp | The account's address |
Every failure — an unknown token, a wrong code, an expired or already-used
record, or a record whose target is no longer the account's current address —
answers the same 400 AUTH_VERIFICATION_CODE_INVALID, so the endpoint cannot be
used to learn which addresses have accounts or which addresses are pending
elsewhere. POST /api/auth/email/verify/resend answers identically (200, no
body change) for an unknown address, an already-verified one, and a real send.
3.3 Account invitations
POST /api/auth/invitation/accept is the whole of account setup. It verifies the
email address and sets the first password in one transaction, and the account is
usable immediately afterwards — there is no second verification email, and the
recipient is never sent to the forgot-password, reset-password, verify-account or
resend-verification screens.
Public. The recipient has no session; the authorization statement is possession of the token, which is 48 bytes of entropy and single-use.
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | Yes | The value from the invitation link's token query parameter |
password | string | Yes | 8-32 characters, and must satisfy the strong-password rule |
There is no otp field and no email field. An invitation is redeemable by link
only — its OTP is minted but never rendered into the email, and the OTP lookup is
scoped to password_reset, so a code presented here could never select the record
it names. The token already carries the identity, so asking for an address beside
it would only invite a caller to disagree with it.
Response (200 OK) carries no tokens:
{
"message": "Your account is ready. Sign in with your new password.",
"data": null,
"errorCode": null
}Accepting an invitation deliberately does not mint a session. The flow ends at the sign-in screen, so a leaked invitation costs an attacker a password they must then use at the front door rather than a session they already hold.
Errors:
| Status | errorCode | Meaning | What the consumer should do |
|---|---|---|---|
| 400 | AUTH_ACCOUNT_INVITE_EXPIRED | The link is older than seven days, or the token is unknown | Ask an administrator to send a fresh invitation |
| 400 | AUTH_ACCOUNT_INVITE_ALREADY_USED | The password has already been set from this link | Send the person to sign in |
| 400 | AUTH_ACCOUNT_INVITE_REVOKED | The account was disabled after the invitation was sent (canLogin is false) | Contact an administrator; a fresh invitation will not help |
| 400 | AUTH_VERIFICATION_CODE_INVALID | The account's address no longer matches the one the invitation was minted for | Ask for a fresh invitation to the current address |
| 401 | AUTH_ACCOUNT_BANNED | The account was banned after the invitation was sent | Contact an administrator |
| 429 | — | Rate limit exceeded | Ten attempts per fifteen minutes per caller |
The token is single-use and the claim is atomic: two requests redeeming one link
concurrently produce exactly one success and one AUTH_ACCOUNT_INVITE_ALREADY_USED.
Redeeming it also retires the person's other account-entry credentials, so an
administrator-issued reset sent alongside an invitation cannot stay live once
either has been used.
Address changes invalidate the invitation. The endpoint asserts that the account's current address still equals the one the invitation was minted for, rather than assigning the invitation's address to the account. An invitation sent to an address the account no longer holds is refused, which is what stops a link sent to a former address from taking over the account it was issued against.
3.4 Logout
DELETE /api/auth/logout and DELETE /api/mobile/auth/logout end the calling
session with reason user_logout. The same session-ending path runs
internally, with reason session_ended, when POST /api/auth/refresh or
POST /api/mobile/auth/refresh finds the session expired, the presented
refresh token mismatched, or the user record missing — never as a result of
somebody choosing to sign out.
Ending a session also retires that session's push tokens, recording the same reason on them, because a token stays deliverable only for as long as the session that registered it is alive. See the notification device endpoints for the full lifecycle; this route's own response is unaffected either way.
3.3a Self-service email change
Four routes under email/change, both mounts. No parameterised route shares the
prefix, so none can shadow another.
POST /api/auth/email/change
JWT. Rate limit: AUTH_EMAIL_CHANGE_REQUEST — 8 requests an hour per person
(auth tier — fails closed on a Redis outage). Refused requests count, and "Send a
new code" comes back through this route. Separately, one code is issued per person
per 60 seconds; a second request inside that window answers
429 AUTH_VERIFICATION_RESEND_COOLDOWN.
{
"newEmail": "new.address@example.com",
"currentPassword": "CurrentPass@123"
}| Field | Type | Required | Notes |
|---|---|---|---|
newEmail | string | Yes | Trimmed and lower-cased; must be a plain dot-atom address (local@domain.tld) — a quoted local part, which @IsEmail() alone would accept, is refused because it can carry a CR/LF |
currentPassword | string | Yes | Up to 128 characters |
Response (200 OK):
{
"message": "We sent a confirmation link and code to the new address.",
"data": { "sentTo": "n•••@example.com", "expiresAt": "2026-09-25T14:30:00.000Z" },
"errorCode": null
}Nothing on the account changes yet. A confirmation link and a 6-digit code go to the new address; if the account already has a verified address, a notice with a cancel link goes there too.
Errors:
| Status | errorCode | Meaning | What the consumer should do |
|---|---|---|---|
| 400 | AUTH_EMAIL_UNCHANGED | newEmail is already the account's address | Nothing to do |
| 400 | AUTH_CURRENT_PASSWORD_REQUIRED | The account has no password set | Set a password first |
| 400 | AUTH_CURRENT_PASSWORD_INCORRECT | Wrong current password | Ask again; this does not spend an attempt or the cooldown |
| 409 | AUTH_EMAIL_ALREADY_EXISTS | Another account already holds newEmail | Choose a different address |
| 429 | AUTH_VERIFICATION_RESEND_COOLDOWN | A code was requested for this account within the last 60 seconds | Wait, then retry |
| 429 | — | Rate limit exceeded | Back off |
POST /api/auth/email/change/confirm-code
JWT. Rate limit: AUTH_OTP_VERIFY (auth tier).
{ "otp": "123456" }Confirms the caller's own pending change. Every session ends, including this one — sign in again with the new address.
Response (200 OK):
{
"message": "Email changed. Sign in again with your new address.",
"data": { "email": "new.address@example.com" },
"errorCode": null
}Errors: 400 AUTH_VERIFICATION_CODE_INVALID (unknown/wrong/expired code,
or no pending change), 400 AUTH_VERIFICATION_ATTEMPTS_EXCEEDED (ten wrong
codes across any number of reissues within 24 hours, or the per-record cap),
403 AUTH_ACCOUNT_BANNED, 400 AUTH_ACCOUNT_SIGN_IN_REVOKED,
409 AUTH_EMAIL_ALREADY_EXISTS (another account took the address after the
request was made).
POST /api/auth/email/change/confirm
Public. Rate limit: AUTH_ATTEMPT_GUEST. Opened from the link, without a
session — so the current password is required too, or whoever merely reads the
new mailbox could complete the change alone.
{ "token": "<from the link>", "currentPassword": "CurrentPass@123" }Same response shape and the same session-ending behaviour as confirm-code.
Errors: the confirm-code set above, plus 401 AUTH_ACCOUNT_BANNED (a 401
here, not the 403 the signed-in route uses, because there is no session to
distinguish it from) and 400 AUTH_CURRENT_PASSWORD_INCORRECT — a wrong
password here spends one of the record's attempts, the same budget a wrong code
would.
POST /api/auth/email/change/cancel
Public. Rate limit: AUTH_ATTEMPT_GUEST. Opened from the cancel link sent to
the old address.
{ "token": "<from the cancel link>" }Response (200 OK) — three outcomes, distinguished only by data.cancelled
and status:
{ "message": "The email change was cancelled.", "data": { "cancelled": true }, "errorCode": null }cancelled: false means nothing was pending (already cancelled, expired, or the
token does not resolve). A change that had already been confirmed is not
reported as cancelled: false — it is a distinct 409, because the address is
already gone and the caller needs to know that resetting the password, not
retrying this link, is the next step:
{
"message": "This change was already completed. If you did not make it, reset your password and contact the school.",
"data": null,
"errorCode": "AUTH_EMAIL_CHANGE_ALREADY_COMPLETED"
}Every route in this group lives on AuthEmailChangeService, shared byte-for-byte
between /api/auth/* and /api/mobile/auth/*. See
the backend page
for the transaction shape, lock order, and the Redis-backed cooldown and lockout
these routes share with every other verification record.
3.5 GET /api/auth/permissions
Returns the permission codes of the caller's active role as a flat string[] in the standard
envelope. It never returns the union of every role the account holds.
| Active role | data |
|---|---|
| None (no active role on the token) | [] |
A role with isSuperadmin: true | Every catalogue code except the person-bound screen codes |
| Any other role | The codes granted to that role in role_permission |
{
"message": "Permissions fetched.",
"data": ["Students_READ", "Students_CREATE", "NoticeView_READ", "..."]
}The superadmin list. buildSuperadminPermissionCatalog() in
packages/db/src/authorization/permission-catalog.ts builds it: the full catalogue with every code
of the modules in PERSON_BOUND_SCREEN_MODULES removed — Schedule, ExamView,
AcademicHistory, AttendanceView, LeaveView, MyFees, MySalary, MyAssets,
AssignmentView and MyReports, all five actions of each. Those codes gate the panel's "My …"
screens, which show the caller's own student, guardian or staff record, and a superadmin account has
none. NoticeView and MyComplaints stay in the list: notices reach a superadmin through the
all_users, role and users audiences, and complaints have an admin filer path for an account
with no staff row.
The list is narrower than what the route guard enforces, and only in the non-granting direction.
RoleGuard still admits a superadmin active role to every route, so no API route changes behaviour.
The panel builds its sidebar and its page gates from this list alone, which is what hides the
screens. A superadmin who also holds a staff or teacher role switches active role to reach them.
4. Set Password
Authenticated endpoints for setting or changing a user's password.
4.1 POST /api/auth/password/set
4.2 POST /api/mobile/auth/password/set
Set or change password for an authenticated user.
Authentication: JWT Bearer token
Rate limit: 5 requests per 15 minutes
Request body:
{
"newPassword": "NewPass@1234",
"currentPassword": "CurrentPass@123"
}currentPassword is required only if the user already has a password.
Response (200 OK):
{
"message": "Password set successfully.",
"data": null,
"errorCode": null
}Errors:
400 Bad Request—currentPasswordmissing when user already has password401 Unauthorized— Invalid JWT or incorrectcurrentPassword429 Too Many Requests— Rate limit exceeded
5. hasPassword Field
All profile responses from:
GET /api/auth/meGET /api/mobile/auth/me
include hasPassword: boolean.
hasPassword: falsemeans no EMAIL provider password exists yet.hasPassword: truemeans EMAIL provider password is set.
Frontend integration:
hasPassword === false→ show "Add Password"hasPassword === true→ show "Change Password"
Data Transfer API Reference
Complete API contracts for Data Transfer, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Auth Module Backend Documentation
Internal auth architecture with split admin/customer controllers for login by email or mobile number, sessions, password reset, and verification.