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