---
feature: Entry, Nafath registration, login, affiliated first login
slug: auth
surface: trainer
consumer: flutter
version: 1.0.0
status: ready
brd_version: "v1.0 (11-8-2026) + additional requirements"
brd_ids: [TRR-001, TRR-002, TRR-003, TRR-004, PLT-003, PLT-005]
business_rules: [BR-ACC-01, BR-CTR-04, BR-ACC-05]
decisions: [TD-08, TD-15, TD-16, DEC-14, TD-21, TD-22]
schema_tables: [users, trainers, nafath_verifications]
related_docs: [profile.md, ../admin/trainers.md]
last_updated: 2026-11-15
---

# Entry, Nafath registration, login, affiliated first login — Trainer App Integration Guide

> **Base URL:** `{HOST}/api/v1/trainer` · Shared rules: [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Quick reference

| | |
|---|---|
| Version | 1.0.0 (2026-11-15) — first release; app action: build registration, Nafath, login and reset screens |
| Status | ready · BRD TRR-001..004, PLT-003 · DEC-14, TD-21, TD-22 · Nafath runs on the `fake` driver (TD-16) |
| Auth | Register / verify / resend / login / forgot / reset are public. The rest need the trainer's Bearer token; another surface's token gets 403 (TD-08). |

| Method | Path | Route name | Purpose |
|--------|------|------------|---------|
| `POST` | `/auth/register` | `trainer.auth.register` | Step 1: ID, phone, password → OTP sent |
| `POST` | `/auth/verify` | `trainer.auth.verify` | Confirm the OTP → account + token |
| `POST` | `/auth/resend-code` | `trainer.auth.resend` | Resend the OTP |
| `POST` | `/auth/login` | `trainer.auth.login` | Phone / email / center username + password |
| `POST` | `/auth/forgot-password` | `trainer.auth.forgot` | Send a reset code |
| `POST` | `/auth/reset-password` | `trainer.auth.reset` | Code + new password |
| `POST` | `/auth/logout` | `trainer.auth.logout` | Revoke the token |
| `POST` | `/nafath` | `trainer.nafath.start` | Request identity verification (independent only) |
| `GET` | `/nafath` | `trainer.nafath.status` | Latest verification; checks a pending one |

**Statuses (trainer):** `profile_incomplete` → `under_review` → `active` · `rejected` (edit and resubmit) · `suspended` (no login). Nafath: `pending` · `verified` · `failed` · `expired`.
**Error codes:** `ACCOUNT_INACTIVE` (403, suspended) · `BUSINESS_RULE_VIOLATED` (422) · validation 422 · 429 (login throttled, `Retry-After`).
**Open decisions:** DEC-14 — how a center issues an affiliated trainer's first password (CTR-005, center surface, not built). The real Nafath vendor is not supplied (TD-16).

## 1. Business summary

An independent trainer creates the account in two steps: ID / iqama + mobile + password, then the 4-digit code sent to the mobile (fixed `1111` until SMS is connected). Nothing is created before the code is confirmed. The new account is `profile_incomplete`: the app then verifies the identity through Nafath (the official name, date of birth, gender, nationality and ID expiry are copied onto the account and locked) and completes the profile ([profile.md](profile.md)). An affiliated trainer never registers: the center creates the account and the trainer logs in with the username the center gave, then completes the profile (TRR-004). Until the account is `active` the trainer can only use `auth`, `nafath` and `profile`; every other endpoint answers 422 with the reason for the current status.

## 2. User flow

| Step | Screen | Endpoint | Notes |
|------|--------|----------|-------|
| 1 | Entry (TRR-001) | — | Two buttons: create an independent account · log in |
| 2 | Registration | `POST /auth/register` | Keep `verification_token` |
| 3 | OTP | `POST /auth/verify`, `POST /auth/resend-code` | Returns the token; the app is now signed in |
| 4 | Identity | `POST /nafath`, `GET /nafath` | Poll `GET` while `pending` |
| 5 | Profile | [profile.md](profile.md) | Route by `trainer.must_complete_profile` |
| 6 | Login | `POST /auth/login` | Route by `trainer.status` |

## 3. Endpoints

### 3.1 Register — `POST /auth/register` (throttle 10/min)

| Field | Type | Required | Validation |
|-------|------|----------|-----------|
| `national_id` | string | yes | 10 digits starting with 1 (citizen) or 2 (resident); not registered yet |
| `phone` | string | yes | Saudi mobile, 9 digits starting with 5; a leading 0 is dropped; not registered yet |
| `phone_code` | string | no | `966` (default) |
| `password`, `password_confirmation` | string | yes | min 8, max 72 |

```json
{"status":"success","message":"A verification code has been sent to your phone.","data":{"verification_token":"trainer-register:2202926d-f44e-484b-b9b5-f072ca991464"}}
```

### 3.2 Verify — `POST /auth/verify` (throttle 10/min)

`{ "token": "<verification_token>", "code": "1111" }` → 201 with the signed-in account. `data.trainer` tells the app where to go; `data.name` is the ID number until Nafath replaces it with the official name.

```json
{"status":"success","message":"Account created. Verify your identity and complete your profile.","data":{"id":"a33aebf0-f4f1-42a3-93f3-f32f26da9be8","name":"1000000001","email":null,"phone_code":"966","phone":"500000001","country":null,"image":"https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200","user_type":"Trainer","is_active":true,"is_verified":false,"is_banned":false,"is_suspended":false,"settings":{"locale":"ar","allow_notifications":true},"location":{"lat":null,"lng":null},"token":"<jwt>","verification_token":null,"permissions":[],"trainer":{"id":"a33aebf0-f7d5-4538-9c9a-82cb61561767","type":{"value":"independent","label":"مستقل"},"status":{"value":"profile_incomplete","label":"الملف غير مكتمل"},"must_complete_profile":true,"can_operate":false,"rejection_reason":null}}}
```

A wrong or expired code, or a token not issued by `register`, is a 422. A number registered in the meantime is a 422 on `phone`.

### 3.3 Login — `POST /auth/login`

`{ "login": "<phone | email | username>", "password": "..." }`. An independent trainer uses the phone (`501234567`, `0501234567`, `+966501234567`) or email; a center-created trainer uses the username the center issued. Credentials are matched among trainer accounts only (TD-21). Throttled per login + IP (429). A suspended trainer gets 403 `ACCOUNT_INACTIVE`.

```json
{"status":"success","message":"You logged in successfully","data":{"id":"a33aebf0-f4f1-42a3-93f3-f32f26da9be8","name":"1000000001","email":null,"phone_code":"966","phone":"500000001","country":null,"image":"https://ui-avatars.com/api/?name=U&background=dbeafe&color=1d4ed8&size=200","user_type":"Trainer","is_active":true,"is_verified":false,"is_banned":false,"is_suspended":false,"settings":{"locale":"ar","allow_notifications":true},"location":{"lat":null,"lng":null},"token":"<jwt>","verification_token":null,"permissions":[],"trainer":{"id":"a33aebf0-f7d5-4538-9c9a-82cb61561767","type":{"value":"independent","label":"مستقل"},"status":{"value":"profile_incomplete","label":"الملف غير مكتمل"},"must_complete_profile":true,"can_operate":false,"rejection_reason":null}}}
```

### 3.4 Forgot / reset password

`POST /auth/forgot-password` `{ "login": "<phone | email>" }` → `{ "verification_token": "reset:<uuid>" }` (same answer for an unknown account). `POST /auth/reset-password` `{ "token", "code", "password", "password_confirmation" }`. Tokens issued before the reset stop working.

### 3.5 Nafath — `POST /nafath`, `GET /nafath`

Independent trainers only (an affiliated trainer gets 422). `POST` starts verification for the ID registered in step 1 (throttle 6/min) and returns the stored result when the trainer is already verified, so it is safe to repeat. With the fake driver the last digit of the ID decides the outcome: 1 verified, 2 failed, 3 expired, anything else stays `pending` (then `GET` checks it).

```json
{"status":"success","message":"Verification requested.","data":{"id":"a33aebf0-ffba-4514-9b35-6e6d611cc65e","status":{"value":"verified","label":"موثق"},"full_name_ar":"مدرب موثق","full_name_en":"Verified Trainer","birth_date":"1990-01-01","gender":"male","id_expires_on":"2029-11-15","verified_at":"2026-11-15T09:23:38+00:00"}}
```

On `verified`, name, date of birth and gender go to the account, ID number, nationality and expiry to the trainer, and the result with its timestamp is kept. These fields are never accepted by `PUT /profile`.

## 4. Business rules enforced

| Rule | Behaviour |
|------|-----------|
| BR-ACC-01 | Independent trainers self-register; the account exists only after the OTP. |
| BR-CTR-04 | An affiliated trainer never registers here. |
| PLT-003 | Nafath is for independent trainers; the result and status are kept. |
| TD-08 | Tokens of other surfaces are refused (403); a password change or reset signs out older tokens. |
| TD-21 | Login and reset look the account up inside the trainer type only. |

## 5. Known limitations

- The password is asked at step 1 (the BRD lists it on the profile form) so an abandoned registration can log back in (TD-22).
- OTP delivery is logged, not sent, until an SMS provider is connected; the code is fixed `1111`.
- Center-created trainer accounts (CTR-005) are not built; they exist through seeders / factories only.

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.0.0 | 2026-11-15 | Initial | Registration with OTP, Nafath (fake driver), login, reset, logout. | Build the screens above. |

## Changelog

### 1.0.0 — 2026-11-15
- **Added:** `auth/*`, `nafath`; `SurfaceCredentials`, `SurfacePasswordReset`, `LockTrainerIdentity`.
- **Breaking:** No
- **App action required:** Build the screens above.
- **BRD / decisions:** TRR-001..004, PLT-003, DEC-14, TD-21, TD-22
