Files

8.9 KiB
Raw Permalink Blame History

افزودن اسم سایت شهر به پیامک کد تأیید (OTP) — به‌صورت اختیاری

پروژه

clinicpro (Backend) — cross-repo؛ پرامپت همتای frontend: nobat724_front/.claude/prompt/otp-sms-site-name.md

زمینه

سایت عمومی چند-دامنه‌ای است (هر شهر یک دامنه: yasuj-nobat.ir، ahvaz-nobat.ir، ...). هر شهر در جدول cities فیلد site_name دارد (مثل «یاسوج نوبت»). الان پیامک کد تأیید ورود ثابت است: کد تأیید شما: {code} و اسم سایت شهر ندارد.

هدف: بتوان اختیاری اسم سایتِ شهرِ درخواست‌کننده را در پیامک OTP نمایش داد — مثلاً «کد تأیید شما در یاسوج نوبت: ۱۲۳۴۵». اختیاری‌بودن از طریق قالب قابل‌ویرایش پنل SMS کنترل می‌شود: اگر ادمین {site} را در متن قالب بگذارد نمایش داده می‌شود، نگذارد نمی‌شود.

مشکل / هدف

  • درخواست send-code از دامنه‌ی شهر به API (api.clinic-pro.ir) می‌رسد → Host = دامنه‌ی API، نه شهر. پس backend خودش شهر را نمی‌فهمد.
  • راه‌حل: frontend دامنه‌ی خودش را در body می‌فرستد؛ backend از روی domain در جدول citiessite_name را پیدا می‌کند و به‌عنوان متغیر {site} به قالب پیامک تزریق می‌کند. اگر شهری پیدا نشد یا domain نیامد → fallback به برند پیش‌فرض «نوبت ۷۲۴».
  • متغیر {site} همیشه مقداردهی می‌شود؛ استفاده یا عدم‌استفاده‌اش به قالب بستگی دارد (اختیاری).

فایل‌های مرتبط

فایل نقش
src/Auth/Controller/AuthController.php endpoint POST /api/v1/user/send-code — خواندن domain اختیاری از body
src/Auth/Service/OtpService.php ساخت و ارسال پیامک OTP — resolve اسم سایت و تزریق {site}
src/Location/Repository/CityRepository.php lookup شهر بر اساس domain
src/Location/Entity/City.php دارای getDomain() و getSiteName() (آماده، تغییر نمی‌خواهد)
src/Sms/Entity/SmsMessageTemplate.php ثبت {site} به‌عنوان متغیر مجاز قالب OTP
docs/api/auth.md, docs/api/sms.md مستندسازی

وضعیت فعلی

OtpService::sendCode (src/Auth/Service/OtpService.php):

public function sendCode(string $mobile): string
{
    $uuid = Uuid::v4()->toRfc4122();
    $code = $this->appEnv === 'dev' ? '12345' : str_pad((string) random_int(10000, 99999), 5, '0', STR_PAD_LEFT);
    // ... cache set ...
    if ($this->appEnv !== 'dev') {
        $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code]);
        $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
    }
    return $uuid;
}

AuthController::sendCode (src/Auth/Controller/AuthController.php:136):

$data   = json_decode($request->getContent(), true) ?? [];
$mobile = trim($data['mobile'] ?? '');
// ... validation + rate limit ...
$uuid = $this->otpService->sendCode($mobile);

قالب پیش‌فرض OTP (src/Sms/Entity/SmsMessageTemplate.php):

SmsLog::TAG_OTP => [
    'title'     => 'کد تأیید ورود',
    'body'      => 'کد تأیید شما: {code}',
    'variables' => ['code'],
],

resolver (src/Sms/Service/SmsTextResolver.php) هر {key} را با str_replace جایگزین می‌کند — پس {site} بدون تغییر resolver کار می‌کند.

وظایف

۱. OtpService — resolve اسم سایت و تزریق {site}

  • CityRepository را به constructor تزریق کن.
  • امضای متد را به sendCode(string $mobile, ?string $domain = null) تغییر بده (پارامتر اختیاری → سازگاری عقب‌رو).
  • اسم سایت را resolve کن و به متغیرهای قالب اضافه کن:
public function sendCode(string $mobile, ?string $domain = null): string
{
    // ... code + cache بدون تغییر ...
    if ($this->appEnv !== 'dev') {
        $site = $this->resolveSiteName($domain);
        $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]);
        $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
    }
    return $uuid;
}

private function resolveSiteName(?string $domain): string
{
    $default = 'نوبت ۷۲۴';
    if (!$domain) {
        return $default;
    }
    $domain = strtolower(trim(preg_replace('#^https?://#', '', $domain)));
    $domain = preg_replace('#/.*$#', '', $domain); // strip path
    $city = $this->cityRepo->findOneBy(['domain' => $domain]);
    return $city?->getSiteName() ?: $default;
}
  • نکته: {site} همیشه مقدار دارد؛ اگر قالب فعلی {site} نداشته باشد، این متغیر بی‌اثر است (اختیاری‌بودن حفظ می‌شود).

۲. AuthController::sendCode — خواندن domain اختیاری از body

$mobile = trim($data['mobile'] ?? '');
$domain = isset($data['domain']) ? trim((string) $data['domain']) : null;
// ... validation + rate limit بدون تغییر ...
$uuid = $this->otpService->sendCode($mobile, $domain ?: null);
  • domain اختیاری است؛ اگر نیامد، رفتار قبلی حفظ می‌شود.
  • روی domain validation سخت‌گیرانه لازم نیست (فقط برای lookup استفاده می‌شود، نه اجرا)، اما trim و طول محدود (مثلاً حداکثر ۲۵۳ کاراکتر) اعمال کن.

۳. SmsMessageTemplate — ثبت {site} به‌عنوان متغیر مجاز OTP

قالب پیش‌فرض را بدون {site} نگه دار (opt-in)، اما site را به لیست variables اضافه کن تا در پنل ویرایش قالب به‌عنوان متغیر قابل‌استفاده نمایش داده شود:

SmsLog::TAG_OTP => [
    'title'     => 'کد تأیید ورود',
    'body'      => 'کد تأیید شما: {code}',   // پیش‌فرض بدون site
    'variables' => ['code', 'site'],          // site به‌عنوان متغیر اختیاری در پنل
],
  • این‌طوری ادمین در پنل SMS می‌تواند body را به کد تأیید شما در {site}: {code} تغییر دهد تا اسم سایت نمایش داده شود — یا ندهد.

۴. مستندسازی

  • docs/api/auth.md: در POST /api/v1/user/send-code، فیلد اختیاری domain را به request body اضافه کن (نوع string، توضیح: دامنه‌ی شهرِ درخواست‌کننده برای شخصی‌سازی متن پیامک؛ اگر نیامد از برند پیش‌فرض استفاده می‌شود). نمونه request/response را به‌روز کن.
  • docs/api/sms.md: متغیر جدید {site} را برای قالب OTP مستند کن (مقدار = site_name شهرِ دامنه یا «نوبت ۷۲۴»).

نکات مهم

  • backward-compatible: هر دو domain (body) و پارامتر sendCode اختیاری‌اند؛ کلاینت‌های فعلی (پنل ادمین clinicpro، لاگین بدون domain) بدون تغییر کار می‌کنند و برند پیش‌فرض را می‌گیرند.
  • محیط dev: پیامک ارسال نمی‌شود (کد ثابت 12345) — این مسیر دست‌نخورده بماند.
  • {site} هرگز خالی نباشد (fallback «نوبت ۷۲۴») تا اگر ادمین {site} را در قالب گذاشت، متن ناقص «... در : ...» تولید نشود.
  • lookup دامنه: مقدار ارسالی frontend ممکن است yasuj-nobat.ir یا با www./https:// باشد — در resolveSiteName normalize کن (حذف پروتکل و path). اگر لازم شد www. را هم strip کن، اما مقادیر cities.domain بدون www ذخیره شده‌اند (مثل yasuj-nobat.ir).
  • پیامک OTP با پترن اپراتور: provider پترن {site} را پشتیبانی می‌کند (تأییدشده). مطمئن شو ترتیب/نام متغیرها با پترن مصوب سامانه‌ی پیامکی هماهنگ است.
  • تست:
    ddev exec php -l src/Auth/Service/OtpService.php
    ddev exec php -l src/Auth/Controller/AuthController.php
    ddev exec php bin/console cache:clear
    # ارسال با domain و بدون domain را در محیط staging بررسی کن (dev پیامک نمی‌فرستد)
    
  • بعد از تغییر API، docs/api/auth.md و docs/api/sms.md را در همین session به‌روز کن (قانون استاندینگ پروژه).