Files
clinicpro/.claude/prompt/fix-server-side-mobile-nationalcode-validation.md

8.5 KiB
Raw Permalink Blame History

رفع باگ: اعتبارسنجی سمت سرورِ موبایل و کد ملی (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 (mobilecreateClinic (owner_mobile) — همان خلأ
src/Admin/Controller/AdminApiController.php createDoctor (mobilecreateClinic (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 چک می‌شود:

$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 ذخیره می‌شود:

if (array_key_exists('national_code', $data))  $profile->setNationalCode($data['national_code']);

regex موبایلِ موجود (برای الگو):

// AuthController:143 و NotificationMobileController:44
if (!preg_match('/^09\d{9}$/', $mobile)) { ... 422 ... }

وظایف

۱. سرویس مشترک InputValidator

src/Shared/Service/InputValidator.php بساز با متدهای static (یا سرویسِ بدون state):

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:

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 در ورودی هست و غیرخالی است، اعتبارسنجی کن:

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 (قبلاً ۲۰۱).
    • با موبایل معتبر 09123456789201 و در DB لاتین ذخیره شود.
    • PATCH /api/v1/user-profile/{own} با national_code:"1111111111"422؛ با کد معتبر (مثل 0079070875) → 200؛ با null200 (پاک شود).
    • رگرسیون: createDoctor/createClinic ادمین و نماینده با موبایل معتبر همچنان کار کنند.
  • بعد از تست، رکوردهای تستی را پاک کن (هیچ نماینده/کاربر/پروفایلِ آشغال نماند).