Files
clinicpro/docs/api/auth.md
T
hamed 57aeb40934 feat: add staff role functionality with dashboard access and service management
- Implemented SidebarStaff component tests to ensure staff users see only their dashboard and services.
- Created StaffMyServicesPage to display assigned services for staff users.
- Added migration to link clinic staff rows to user accounts for ROLE_STAFF access.
- Defined StaffPermissions class for static permissions related to staff role.
- Introduced StaffRouteGuardSubscriber to restrict API access for staff users.
- Developed StaffAccountService for managing staff user accounts and linking them to clinic staff.
- Added comprehensive tests for StaffAccountService to validate user creation, mobile number handling, and account attachment.
- Implemented tests for staff dashboard access to ensure proper permissions and access control.
- Created tests for staff login context to verify correct environment visibility based on user roles.
2026-07-30 10:18:41 +03:30

739 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Authentication API
> **Prefix:** `/api/v1/user` and `/oauth`
> **Permission:** All endpoints in this module are **PUBLIC** (no JWT required) except `userinfo` and `logout`
> **🛡️ ALTCHA captcha:** وقتی `ALTCHA_ENABLED=true` است (در prod)، endpointهای `send-code`، `register`، `otp-login` و `reset-password` علاوه بر بدنه‌ی خود، فیلد `altcha` (payload حل‌شده‌ی widget) را الزامی می‌کنند؛ در غیر این صورت `422` با کد `ERR_CAPTCHA_001` برمی‌گردد. جزئیات و مسیر challenge در [captcha.md](captcha.md).
---
## POST `/api/v1/user/send-code`
Send OTP code to mobile number.
**Permission:** `PUBLIC`
### Request Body
```json
{
"mobile": "09123456789",
"domain": "yasuj-nobat.ir"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `mobile` | string | ✅ | Format: `09XXXXXXXXX` (11 digits) |
| `domain` | string | ⬜ | دامنه‌ی شهرِ درخواست‌کننده (multi-domain). اگر ارسال شود، `site_name` شهرِ متناظر در جدول `cities` پیدا شده و به‌عنوان متغیر `{site}` در متن پیامک OTP قابل استفاده است (شخصی‌سازی متن). اگر نیامد یا شهر پیدا نشد → مقدار پیش‌فرض «کلینیک پرو». حداکثر ۲۵۳ کاراکتر. |
### Response `200`
```json
{
"success": true,
"data": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"message": "کد تأیید ارسال شد"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Invalid mobile format |
| `ERR_AUTH_004` | 429 | OTP rate limit exceeded |
| `ERR_RATE_LIMIT_001` | 429 | بیش از حد مجاز — هم per-IP (۵ در ۶۰ دقیقه) و هم **per-mobile** (۵ در ۶۰ دقیقه؛ ضد flood از IPهای چرخشی) |
---
## POST `/api/v1/user/verify-code`
Verify OTP code. Returns whether this is a new or existing user.
**Permission:** `PUBLIC`
### Request Body
```json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"code": "123456"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | ✅ | UUID returned from `send-code` |
| `code` | string | ✅ | 6-digit OTP |
### Response `200`
```json
{
"success": true,
"data": {
"message": "کد با موفقیت تایید شد.",
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"is_new_user": false
}
}
```
> `grant` یک توکنِ **یک‌بارمصرف و کوتاه‌عمر (۱۲۰ ثانیه)** است که در مرحله‌ی بعد (`register` / `oauth/token` / `otp-login` / `reset-password`) مصرف می‌شود. خودِ OTP (`uuid`) پس از تأیید موفق حذف می‌شود. هر grant فقط یک‌بار قابل‌استفاده است؛ هر action که grant می‌خواهد یک grant جداگانه لازم دارد (یعنی برای new-user که هم `register` و هم login لازم است، باید دوبار verify انجام شود یا مستقیماً `oauth/token` صدا زده شود که خودش کاربر را می‌سازد).
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_002` | 400 | Invalid OTP code |
| `ERR_AUTH_003` | 400 | OTP expired |
| `ERR_VALIDATION_002` | 422 | Missing required field |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ تلاش در ۱۵ دقیقه (per-IP) |
---
## POST `/api/v1/user/register`
Complete registration for new users (called only when `is_new_user: true`).
**Permission:** `PUBLIC`
### Request Body
```json
{
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"real_name": "علی احمدی"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
| `real_name` | string | ❌ | User's full name |
### Response `201`
```json
{
"success": true,
"data": {
"message": "ثبت‌نام با موفقیت انجام شد",
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | Missing grant |
| `ERR_AUTH_002` | 400 | grant نامعتبر یا منقضی/مصرف‌شده |
---
## POST `/api/v1/user/login`
Login with mobile number and password (for users who set a password).
**Permission:** `PUBLIC`
### Request Body
```json
{
"mobile_number": "09123456789",
"password": "mypassword"
}
```
| Field | Type | Required |
|-------|------|----------|
| `mobile_number` | string | ✅ |
| `password` | string | ✅ |
### Response `200`
```json
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
> فقط برای کاربران staff (`ROLE_ADMIN/DOCTOR/CLINIC/SECRETARY`). Access token TTL: **۹۰۰ ثانیه (۱۵ دقیقه)**.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_005` | 401 | Wrong credentials |
| `ERR_AUTH_006` | 403 | کاربر staff نیست |
| `429` | 429 | Too many attempts (۱۰ در دقیقه per-IP) |
---
## POST `/oauth/token`
Exchange a one-time `grant` (from `verify-code`) for a JWT access token. اگر کاربری با آن موبایل وجود نداشته باشد، ساخته می‌شود.
**Permission:** `PUBLIC`
### Request Body
```json
{
"grant_type": "mobile",
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `grant_type` | string | ✅ | Must be `"mobile"` |
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` (عمر ۱۲۰ ثانیه) |
### Response `200`
```json
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def50200...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
> JWT payload: `{ username: mobile_number, roles: [...], iat, exp }`
> Access token TTL: **۹۰۰ ثانیه (۱۵ دقیقه)** | Refresh token TTL: **۳۰ روز** (در cache، هش‌شده و rotate-on-use)
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 400 | `grant_type` نامعتبر |
| `ERR_VALIDATION_002` | 422 | `grant` ارسال نشده |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا قبلاً مصرف‌شده |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ درخواست در ۵ دقیقه (per-IP) |
---
## POST `/oauth/token/refresh`
Refresh expired JWT using refresh token.
**Permission:** `PUBLIC`
### Request Body
```json
{
"refresh_token": "def50200..."
}
```
> **قابل استفاده‌ی مجدد:** refresh token تا انقضای TTL خود معتبر است و در پاسخ **بدون تغییر** برگردانده می‌شود (یک‌بارمصرف/چرخشی **نیست** — سایت عمومی در هر render سمت‌سرور refresh می‌زند و نمی‌تواند توکن چرخش‌یافته را ذخیره کند). کاربر **معلق** (`status != 1`) نمی‌تواند refresh کند (`401`).
### Response `200`
```json
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "<همان توکن ارسالی — بدون تغییر>",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Invalid/expired refresh token, or suspended user (`status != 1`) |
---
## GET `/oauth/userinfo`
Get authenticated user info — extended with multi-context support.
**Permission:** `AUTH` — requires valid JWT
### Headers
```
Authorization: Bearer <token>
```
### Response `200`
```json
{
"success": true,
"data": {
"id": 4766,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"mobile_number": "09123456789",
"realName": "دکتر وحید درویشی",
"status": 1,
"roles": ["ROLE_USER", "ROLE_DOCTOR"],
"primary_role": "doctor",
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"db_key": "hmac-sha256-hash...",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"context": {
"type": "doctor",
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"name": "مطب شخصی دکتر وحید درویشی",
"role": "doctor"
},
"available_contexts": [
{
"type": "doctor",
"db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"name": "مطب شخصی دکتر وحید درویشی",
"role": "doctor"
},
{
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "doctor",
"scope": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966",
"permissions": {
"version": 1,
"resources": {
"appointments": { "view": true, "create": true, "cancel": true, "update_status": true },
"appointment_settings": { "view": true, "update": true },
"patients": { "view": true, "create": true, "update": true, "delete": false },
"payments": { "view": true, "create": false, "update": false, "delete": false },
"services": { "view": true, "update": false },
"clinic_info": { "view": true, "update": false }
}
}
}
]
}
}
```
| فیلد | نوع | توضیح |
|------|-----|-------|
| `primary_role` | string | نقش اصلی: `admin` \| `clinic` \| `doctor` \| `secretary` \| `staff` \| `representation` \| `user` |
| `db_uuid` | string\|null | UUID موجودیت فعال (null = هنوز context انتخاب نشده) |
| `db_key` | string\|null | `HMAC-SHA256(db_uuid, APP_SECRET)` برای اعتبارسنجی |
| `doctor_uuid` | string\|null | UUID دکتر — ثابت است حتی در context کلینیک که `db_uuid` برابر UUID کلینیک است. برای کاربران غیر دکتر: `null` |
| `context` | object\|null | context فعال انتخاب‌شده |
| `available_contexts` | array | همه محیط‌های کاری قابل انتخاب |
> **مالکِ چند کلینیک:** اگر یک کاربر مالک بیش از یک کلینیک باشد، **همهٔ** آن‌ها در این فهرست می‌آیند و به هرکدام می‌شود سوییچ کرد. تا پیش از این فقط اولی می‌آمد و کلینیک دوم اصلاً قابل انتخاب نبود — که با جداسازی محیط یعنی دادهٔ آن کلینیک برای خودِ مالکش هم نامرئی می‌ماند. (`MultiClinicOwnerContextTest`)
**فیلد `permissions` در هر context:**
| حالت context | مقدار `permissions` |
|---|---|
| مطب شخصی پزشک (`type: doctor`، `role: doctor`) | `null` — محیط خودش، محدودیتی ندارد |
| مالک کلینیک (`role: clinic`) | `null` — مالک هرگز محدود نمی‌شود |
| پزشکِ عضو کلینیک (`role: doctor`، `scope: clinic`) | envelope کامل `{version, resources}` از `clinic_doctor_permissions` |
| پزشکِ عضوی که دسترسی‌اش غیرفعال شده | `{version: 1, resources: {}}` — یعنی هیچ دسترسی |
| منشی (`role: secretary`) | envelope کامل از `doctor_secretaries` |
| پرسنل (`role: staff`) | envelope ثابت `{"version":1,"resources":{"services":{"view":true},"appointments":{"view":true}}}` — قابل ویرایش نیست |
نکتهٔ مهم برای کلاینت: **نبودِ `permissions` (یا `null`) یعنی «بدون محدودیت»، نه «بدون دسترسی».** ساختار و کلیدهای مجوز پزشکِ عضو کلینیک در `docs/api/clinic.md` → بخش *Clinic Doctor Permissions* آمده است.
**قانون `primary_role`** (اولویت‌بندی):
- `ROLE_ADMIN``"admin"`
- `ROLE_CLINIC``"clinic"`
- `ROLE_DOCTOR``"doctor"`
- `ROLE_SECRETARY``"secretary"`
- `ROLE_STAFF``"staff"` (پرسنل کلینیک/مطب؛ عمداً بعد از منشی: کاربری که هر دو نقش را دارد منشی می‌ماند)
- `ROLE_REPRESENTATION``"representation"` (نماینده؛ دسترسی محدود به پنل ادمین: افزودن پزشک/کلینیک، نوبت‌های پزشکانِ زیرمجموعه، داشبورد نماینده)
- بقیه → `"user"`
**نقش `staff`** — کاربری که از `POST /api/v1/staff` با `has_account: true` ساخته شده
(رجوع به [staff.md](staff.md)):
- هر ردیف **فعالِ** `clinic_staff` که به این کاربر وصل است، یک context با `role: "staff"` و
`scope` برابر `doctor` یا `clinic` می‌سازد. پرسنلِ غیرفعال هیچ context نمی‌گیرد.
- ورود با رمز مجاز است (`User::isStaff()` شامل `ROLE_STAFF` است).
- دسترسی API این کاربر **پیش‌فرض بسته** است: فقط `GET /api/v1/dashboard/staff`،
`POST /api/v1/auth/switch-context`، `POST /api/v1/user/change-password` و مسیرهای `/oauth/*`؛
بقیهٔ `/api/v1/*` با `ERR_FORBIDDEN_001` و ۴۰۳ رد می‌شود (`StaffRouteGuardSubscriber`).
**قانون `context.role`** — نقشی که در آن محیط کاری فعال است:
- context مطب شخصی دکتر: `"doctor"`
- context کلینیک که دکتر **عضو** آن است (مالک نیست): `"doctor"` + `"scope": "clinic"` — پزشک می‌ماند و فقط نوبت‌های خودش در آن کلینیک را می‌بیند؛ دسترسی مدیریتی پنل کلینیک ندارد
- context کلینیک که دکتر **صاحب** آن است: `"clinic"` (دسترسی کامل مالک)
- context منشی: `"secretary"`
- context پرسنل: `"staff"` + `scope` برابر نوع محیط (`doctor` یا `clinic`)
> **نکته frontend:** پس از `switchContext`، `primaryRole` در store از `context.role` و `scope` از `context.scope` آپدیت می‌شود. وقتی `role:"doctor"` و `scope:"clinic"` است (پزشکِ مهمان)، Sidebar فقط «داشبورد» و «نوبت‌ها» را نشان می‌دهد و مسیرهای مدیریتی (`staff`, `clinic-services`, `subscription`, `my-secretaries`, `my-patients`, `profile`) به داشبورد ریدایرکت می‌شوند. در سمت backend هم endpointهای مدیریتی برای پزشک فقط scope **شخصیِ** خودش را برمی‌گردانند (نه کلینیک) و endpointهای ویرایش کلینیک مالکیت را چک می‌کنند (۴۰۳).
**قانون `db_uuid`**:
- اگر یک context وجود دارد: خودکار فعال می‌شود
- اگر چند context وجود دارد و کاربر هنوز انتخاب نکرده: `null` — frontend باید صفحه انتخاب نشان دهد
- پس از `POST /api/v1/auth/switch-context`: برابر context انتخاب‌شده
> **سمت سرور:** جدول `user_active_context` علاوه بر `db_uuid` ستون `db_type` (`doctor` یا `clinic`) هم دارد که از `available_contexts[].type` پر می‌شود. بدون آن، هر بار حل‌کردن محیط دو کوئری می‌خواست: اول کلینیک با آن uuid، بعد پزشک. این ستون در پاسخ API ظاهر نمی‌شود و قرارداد frontend را عوض نمی‌کند.
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Invalid or missing token |
---
## POST `/api/v1/auth/switch-context`
تغییر محیط کاری فعال — باید از لیست `available_contexts` انتخاب شود.
**Permission:** `AUTH`
### Request Body
```json
{
"db_uuid": "clinic-uuid-..."
}
```
| فیلد | نوع | Required | توضیح |
|------|-----|----------|-------|
| `db_uuid` | string (UUID) | ✅ | UUID محیط کاری از لیست `available_contexts` |
### Response `200`
```json
{
"success": true,
"data": {
"db_uuid": "clinic-uuid-...",
"db_key": "new-hmac-hash...",
"context": {
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966"
}
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing or invalid token |
| `ERR_AUTH_006` | 403 | `db_uuid` در لیست context های این کاربر نیست |
| `ERR_VALIDATION_001` | 422 | `db_uuid` ارسال نشده |
---
## Notification Mobile (OTP)
Endpoints for setting/verifying a separate SMS notification number for doctors and clinics. This number receives appointment SMS notifications instead of the account login mobile.
> **Permission:** All 4 endpoints require `IS_AUTHENTICATED_FULLY` (JWT).
---
## POST `/api/v1/notification-mobile/request-otp`
Request an OTP code to verify a new notification mobile number.
**Permission:** `AUTH`
### Request Body
```json
{
"target": "doctor",
"new_mobile": "09123456789"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `target` | string | ✅ | `doctor` or `clinic` |
| `new_mobile` | string | ✅ | Format: `09XXXXXXXXX` |
### Response `200`
```json
{
"success": true,
"data": {
"message": "کد تأیید ارسال شد",
"expires_in": 300
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Invalid `target` or mobile format |
| `ERR_NOT_FOUND_001` | 404 | No doctor/clinic profile found for this user |
### Notes
- Previous unused OTPs for the entity are deleted before creating a new one
- OTP is 6-digit, expires in 5 minutes
- Sends via SMS asynchronously
---
## POST `/api/v1/notification-mobile/verify`
Verify the OTP and save the notification mobile number.
**Permission:** `AUTH`
### Request Body
```json
{
"target": "doctor",
"otp_code": "123456"
}
```
| Field | Type | Required |
|-------|------|----------|
| `target` | string | ✅ `doctor` or `clinic` |
| `otp_code` | string | ✅ 6-digit code |
### Response `200`
```json
{
"success": true,
"data": {
"notification_mobile": "09123456789",
"message": "شماره اعلان با موفقیت ذخیره شد"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | Invalid target / OTP expired / wrong code |
| `ERR_NOT_FOUND_001` | 404 | No OTP request found or profile not found |
---
## GET `/api/v1/notification-mobile/{target}`
Get the current notification mobile for the authenticated user.
**Permission:** `AUTH`
**Path param:** `target``doctor` or `clinic`
### Response `200`
```json
{
"success": true,
"data": {
"notification_mobile": "09123456789"
}
}
```
- Returns `null` for `notification_mobile` if not set
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | No profile found for this user |
---
## DELETE `/api/v1/notification-mobile/{target}`
Remove the notification mobile number.
**Permission:** `AUTH`
**Path param:** `target``doctor` or `clinic`
### Response `200`
```json
{
"success": true,
"data": {
"message": "شماره اعلان حذف شد"
}
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_NOT_FOUND_001` | 404 | No profile found for this user |
---
## POST `/oauth/logout`
Invalidate the refresh token (stored in Redis).
**Permission:** `AUTH`
### Request Body
```json
{
"refresh_token": "def50200..."
}
```
| Field | Type | Required |
|-------|------|----------|
| `refresh_token` | string | ❌ |
### Response `200`
```json
{
"success": true,
"data": {
"message": "با موفقیت خارج شدید"
}
}
```
---
## POST `/api/v1/pre-registration`
Submit a pre-registration request (doctor or clinic). Public endpoint — no auth required.
**Permission:** Public
> **🛡️ ALTCHA:** وقتی `ALTCHA_ENABLED=true` است، فیلد `altcha` (payload حل‌شده‌ی widget) الزامی است؛ در غیر این صورت `422` با `ERR_CAPTCHA_001`. رجوع به [captcha.md](captcha.md).
### Request Body
```json
{
"type": "independent_doctor",
"name": "علی احمدی",
"mobile": "09121234567",
"info": "متخصص داخلی، ۱۰ سال سابقه"
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `type` | string | ✅ | `independent_doctor` \| `doctor_with_clinic` \| `clinic_manager` |
| `name` | string | ✅ | min 2 chars. عنوان «دکتر» ابتدای نام (و پیشوندهای تکراری «دکتر دکتر …») هنگام ثبت حذف می‌شود؛ نام تمیز ذخیره و در نمایش دوباره «دکتر» افزوده می‌شود. |
| `mobile` | string | ✅ | 1015 chars |
| `info` | string | ❌ | specialty, address, etc. |
**Type meanings:**
| Value | Roles granted on approval | Entities created |
|-------|--------------------------|-----------------|
| `independent_doctor` | `ROLE_DOCTOR` | Doctor |
| `doctor_with_clinic` | `ROLE_DOCTOR` + `ROLE_CLINIC` | Doctor + Clinic |
| `clinic_manager` | `ROLE_CLINIC` | Clinic |
### Response `200`
```json
{
"success": true,
"data": { "uuid": "...", "status": "pending" }
}
```
### Error Codes
| Code | HTTP | Meaning |
|------|------|---------|
| `VALIDATION_ERROR` | 422 | Invalid type / mobile / name |
| `DUPLICATE_REQUEST` | 409 | Pending request already exists for this mobile |
---
## POST `/api/v1/user/otp-login`
ورود با کد OTP تأییدشده — **فقط کاربران موجود**، کاربر جدید ایجاد نمی‌شود.
**Permission:** `PUBLIC`
### Request Body
```json
{ "grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039" }
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
### Response `200`
```json
{
"access_token": "eyJ...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token_expires_in": 2592000
}
```
### Error Codes
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_002` | 422 | grant ارسال نشده |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا مصرف‌شده |
| `ERR_AUTH_005` | 401 | کاربری با این شماره یافت نشد |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۱۰ درخواست در ۵ دقیقه (per-IP) |
---
## POST `/api/v1/user/change-password`
تغییر رمز عبور توسط کاربرِ احرازشده (بدون OTP). رمز فعلی راستی‌آزمایی می‌شود.
**Permission:** `IS_AUTHENTICATED_FULLY`
### Request Body
```json
{
"current_password": "oldpass1234",
"new_password": "newpass1234"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `current_password` | string | ✅ | باید با رمز فعلی مطابقت کند |
| `new_password` | string | ✅ | حداقل ۸ کاراکتر و متفاوت با رمز فعلی |
### Response `200`
```json
{ "success": true, "data": { "message": "رمز عبور با موفقیت تغییر یافت" } }
```
### Errors
| HTTP | Code | field | Description |
|------|------|-------|-------------|
| 422 | `ERR_VALIDATION_001` | `new_password` | رمز جدید کوتاه یا برابر رمز فعلی |
| 422 | `ERR_VALIDATION_001` | `current_password` | رمز فعلی نادرست |
---
## POST `/api/v1/user/reset-password`
تغییر رمز عبور با تأیید هویت از طریق OTP.
**Permission:** `PUBLIC`
### Request Body
```json
{
"grant": "ddf8a5994d6a2768203f15606621e3fa0e968438765e22061721a03d336d4039",
"new_password": "newpass1234"
}
```
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `grant` | string | ✅ | grant یک‌بارمصرف از `verify-code` |
| `new_password` | string | ✅ | حداقل ۸ کاراکتر |
### Response `200`
```json
{
"success": true,
"data": { "message": "رمز عبور با موفقیت تغییر یافت" }
}
```
### Error Codes
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_VALIDATION_001` | 422 | grant ارسال نشده یا رمز کمتر از ۸ کاراکتر |
| `ERR_AUTH_002` | 400 | grant نامعتبر، منقضی یا مصرف‌شده |
| `ERR_NOT_FOUND_001` | 404 | کاربری با این شماره یافت نشد |
| `ERR_RATE_LIMIT_001` | 429 | بیش از ۵ درخواست در ۶۰ دقیقه (per-IP) |