141 lines
8.5 KiB
Markdown
141 lines
8.5 KiB
Markdown
# رفع باگ: اعتبارسنجی سمت سرورِ موبایل و کد ملی (BUG-2 و BUG-3)
|
||
|
||
## پروژه
|
||
|
||
`clinicpro` (Backend). مرجع: `docs/qa/bug-report-2026-06-20.md`.
|
||
|
||
## زمینه
|
||
|
||
ممیزی نشان داد اعتبارسنجی **موبایل** و **کد ملی** فقط در فرانت انجام میشود و سمت سرور غایب است. در نتیجه:
|
||
- **BUG-3 (HIGH):** `POST /api/v1/representation` با `mobile_number:"not-a-mobile"` → `201` و یک نماینده/کاربر آشغال ساخت (فقط `empty()` چک میشود، نه فرمت).
|
||
- **BUG-2 (MEDIUM):** `PATCH /api/v1/user-profile/{uuid}` با `national_code:"1111111111"` (رقم کنترلیِ غلط) → `200` و ذخیره شد (هیچ validationی نیست).
|
||
|
||
regex موبایل از قبل بهصورت inline در دو جا هست (`AuthController:143` و `NotificationMobileController:44`: `^09\d{9}$`) ولی منطق پخش/تکراری است و در نقاط ساختِ کاربر استفاده نشده. هیچ ولیدیتورِ کد ملیِ سمت سروری وجود ندارد.
|
||
|
||
## مشکل / هدف
|
||
|
||
یک سرویس اعتبارسنجی مشترک سمت سرور بساز (`InputValidator`) و آن را در همهی نقاطِ ورودیِ موبایل/کد ملی اعمال کن تا دادهی نامعتبر با `422` رد شود (نه ذخیره/ساخت).
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
|------|-----|
|
||
| `src/Shared/Service/InputValidator.php` (جدید) | اعتبارسنجی مشترک موبایل + کد ملی |
|
||
| `src/Representation/Controller/RepresentationController.php` | `create` — BUG-3، موبایل بدون فرمت |
|
||
| `src/Representation/Controller/RepresentationActionController.php` | `createDoctor` (`mobile`)، `createClinic` (`owner_mobile`) — همان خلأ |
|
||
| `src/Admin/Controller/AdminApiController.php` | `createDoctor` (`mobile`)، `createClinic` (`owner_mobile`) — همان خلأ |
|
||
| `src/UserProfile/Controller/UserProfileController.php` | `hydrate()` — BUG-2، کد ملی بدون validation |
|
||
| `src/Shared/Constant/ErrorCodes.php` | `ERR_VALIDATION_001` (موجود) |
|
||
| `docs/api/representation.md`, `docs/api/admin.md`, `docs/api/user-profile.md` | مستندسازی پاسخ ۴۲۲ |
|
||
|
||
## وضعیت فعلی (کد واقعی)
|
||
|
||
`RepresentationController::create` — فقط empty چک میشود:
|
||
```php
|
||
$mobile = trim($data['mobile_number'] ?? '');
|
||
$fullName = trim($data['full_name'] ?? '');
|
||
if (empty($mobile) || empty($fullName)) {
|
||
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'mobile_number و full_name الزامی است', 422);
|
||
}
|
||
$user = $this->userRepo->findByMobile($mobile);
|
||
if ($user === null) { $user = new User($mobile); ... } // ← موبایل نامعتبر هم کاربر میسازد
|
||
```
|
||
|
||
`UserProfileController::hydrate` — کد ملی بدون validation ذخیره میشود:
|
||
```php
|
||
if (array_key_exists('national_code', $data)) $profile->setNationalCode($data['national_code']);
|
||
```
|
||
|
||
regex موبایلِ موجود (برای الگو):
|
||
```php
|
||
// AuthController:143 و NotificationMobileController:44
|
||
if (!preg_match('/^09\d{9}$/', $mobile)) { ... 422 ... }
|
||
```
|
||
|
||
## وظایف
|
||
|
||
### ۱. سرویس مشترک `InputValidator`
|
||
|
||
`src/Shared/Service/InputValidator.php` بساز با متدهای static (یا سرویسِ بدون state):
|
||
```php
|
||
namespace App\Shared\Service;
|
||
|
||
final class InputValidator
|
||
{
|
||
public static function isValidIranMobile(string $mobile): bool
|
||
{
|
||
return (bool) preg_match('/^09\d{9}$/', self::toEnglishDigits($mobile));
|
||
}
|
||
|
||
/** کد ملی ایران: ۱۰ رقم + الگوریتم رقم کنترلی؛ ارقام یکسان نامعتبر. */
|
||
public static function isValidIranNationalCode(string $code): bool
|
||
{
|
||
$code = self::toEnglishDigits($code);
|
||
if (!preg_match('/^\d{10}$/', $code)) return false;
|
||
if (preg_match('/^(\d)\1{9}$/', $code)) return false;
|
||
$check = (int) $code[9];
|
||
$sum = 0;
|
||
for ($i = 0; $i < 9; $i++) $sum += (int) $code[$i] * (10 - $i);
|
||
$r = $sum % 11;
|
||
return $r < 2 ? $check === $r : $check === 11 - $r;
|
||
}
|
||
|
||
public static function toEnglishDigits(string $s): string
|
||
{
|
||
return strtr($s,
|
||
['۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4','۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9',
|
||
'٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4','٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9']);
|
||
}
|
||
}
|
||
```
|
||
> این دقیقاً معادلِ سرورِ `isValidIranNationalCode`/`sanitizeMobileInput` فرانت است (تا رفتار یکدست شود).
|
||
|
||
### ۲. اعمال در ساخت نماینده (BUG-3)
|
||
|
||
در `RepresentationController::create`، بعد از چک empty و قبل از ساخت `User`:
|
||
```php
|
||
if (!InputValidator::isValidIranMobile($mobile)) {
|
||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422);
|
||
}
|
||
```
|
||
- `$mobile` را به ارقام انگلیسی نرمال هم بکن (`InputValidator::toEnglishDigits`) قبل از `findByMobile`/`new User` تا «۰۹...» فارسی هم درست ذخیره شود.
|
||
|
||
### ۳. اعمال در سایر نقاطِ ساختِ کاربر از موبایل
|
||
|
||
همان چک را اضافه کن به:
|
||
- `RepresentationActionController::createDoctor` (فیلد `mobile`) و `createClinic` (فیلد `owner_mobile`).
|
||
- `AdminApiController::createDoctor` (`mobile`) و `createClinic` (`owner_mobile`).
|
||
هرجا `new User($mobile)` ساخته میشود، قبلش موبایل باید معتبر باشد؛ در غیر این صورت `422`.
|
||
|
||
### ۴. اعمال در کد ملی (BUG-2)
|
||
|
||
در `UserProfileController` (متد `update` یا `hydrate`)، اگر `national_code` در ورودی هست و **غیرخالی** است، اعتبارسنجی کن:
|
||
```php
|
||
if (array_key_exists('national_code', $data) && $data['national_code'] !== null && $data['national_code'] !== '') {
|
||
$code = InputValidator::toEnglishDigits((string) $data['national_code']);
|
||
if (!InputValidator::isValidIranNationalCode($code)) {
|
||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'national_code');
|
||
}
|
||
$data['national_code'] = $code; // ذخیرهی نرمالشده
|
||
}
|
||
```
|
||
> اجازهی خالی/`null` بماند (کد ملی اختیاری است)؛ فقط مقدارِ **واردشدهی نامعتبر** رد شود. این چک باید **قبل از** `hydrate()` و در همان action باشد (چون `hydrate` خروجی JsonResponse ندارد).
|
||
|
||
### ۵. مستندسازی
|
||
|
||
- `docs/api/representation.md` و `docs/api/admin.md`: در createDoctor/createClinic/representation، افزودن پاسخ `422 ERR_VALIDATION_001` برای موبایل نامعتبر.
|
||
- `docs/api/user-profile.md`: افزودن `422 ERR_VALIDATION_001` برای کد ملی نامعتبر در PATCH.
|
||
|
||
## نکات مهم
|
||
|
||
- **یکبار تعریف، همهجا استفاده:** منطق موبایل/کد ملی فقط در `InputValidator` باشد؛ regexهای inline موجود (`AuthController:143`, `NotificationMobileController:44`) را هم میتوان به این سرویس واگذار کرد (اختیاری، اگر ریسک کم بود) ولی **رفتار `send-code` را تغییر نده** مگر مطمئن باشی.
|
||
- نرمالسازی ارقام فارسی/عربی به انگلیسی **قبل از ذخیره** انجام شود تا موبایل/کد ملیِ ذخیرهشده همیشه لاتین باشد.
|
||
- کد ملی **اختیاری** است؛ خالی/null نباید ۴۲۲ بدهد — فقط مقدارِ نامعتبر.
|
||
- پاسخها از `BaseController` (`error(code, msg, 422, field?)`)؛ تاریخها دست نخورد؛ migration لازم نیست (فقط منطق).
|
||
- بعد از تغییر، `cache:clear --env=prod` و تست:
|
||
- `POST /api/v1/representation` با `mobile_number:"not-a-mobile"` → `422` (قبلاً ۲۰۱).
|
||
- با موبایل معتبر `09123456789` → `201` و در DB لاتین ذخیره شود.
|
||
- `PATCH /api/v1/user-profile/{own}` با `national_code:"1111111111"` → `422`؛ با کد معتبر (مثل `0079070875`) → `200`؛ با `null` → `200` (پاک شود).
|
||
- رگرسیون: `createDoctor`/`createClinic` ادمین و نماینده با موبایل معتبر همچنان کار کنند.
|
||
- بعد از تست، رکوردهای تستی را پاک کن (هیچ نماینده/کاربر/پروفایلِ آشغال نماند).
|