Files
clinicpro/.claude/prompt/otp-via-kavenegar-lookup.md
T
hamed 6ade17a5b4 feat: implement OTP sending via Kavenegar VerifyLookup and add image cropping modal
- Added support for sending OTP messages using Kavenegar's VerifyLookup method, ensuring compliance with specified token formatting and template usage.
- Updated OtpService to handle new template parameters and fallback mechanisms.
- Introduced ImageCropModal component for cropping images with a user-friendly interface.
- Created utility function for cropping images and generating downloadable files.
2026-07-04 21:50:55 +03:30

11 KiB
Raw Blame History

ارسال پیامک OTP فقط از طریق Kavenegar VerifyLookup (پترن)

پروژه

clinicpro (Backend — SMS/Auth)

زمینه

پیامک کد تأیید ورود (OTP) الان با متد متن‌آزاد send() کاوه‌نگار (/sms/send.json) ارسال می‌شود. کاوه‌نگار برای ارسال کد تأیید، متد اختصاصی VerifyLookup (/verify/lookup.json) دارد که با پترن مصوب کار می‌کند، روی خطوط اشتراکی هم تحویل مطمئن‌تری دارد و برای OTP توصیه/لازم است. هدف: فقط OTP از این به بعد از طریق Lookup ارسال شود (بقیه پیامک‌ها — welcome، secretary، doctor_appointment، user_template — دست‌نخورده بمانند).

اسپک Kavenegar VerifyLookup (از داکیومنت رسمی)

  • Endpoint: https://api.kavenegar.com/v1/{API-KEY}/verify/lookup.json (GET/POST)
  • پارامترهای اجباری: receptor (موبایل)، token (مقدار کد؛ max 100؛ بدون فاصله؛ بدون آندرلاین/خط جدید)، template (نام پترن مصوب در پنل)
  • پارامترهای اختیاری توکن و قانون فاصله (بحرانی):
    توکن فاصله
    token, token2, token3 بدون فاصله (رد می‌شود)
    token10 حداکثر ۵ فاصله مجاز
    token20 حداکثر ۸ فاصله مجاز
  • type: sms (پیش‌فرض) یا call.
  • پترن باید از قبل در پنل کاوه‌نگار تأیید شده باشد؛ متنِ پیام روی پنل ثابت است (نه از DB).
  • نیازمند اشتراک advanced.

پیامد: کد ۵ رقمی (بدون فاصله) → token. اسم سایت مثل «یاسوج نوبت» (۱ فاصله) → باید در token10 برود (نه token/token2/token3 که فاصله را رد می‌کنند).

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

فایل نقش
src/Auth/Service/OtpService.php ساخت و dispatch پیامک OTP — باید به Lookup سوییچ شود
src/Sms/Service/SmsService.php dispatchAsync(...templateCode, templateVars...) → اگر templateCode باشد sendTemplate() صدا می‌زند
src/Sms/Provider/KavehNegarProvider.php sendTemplate()/verify/lookup.json؛ map توکن باید نام‌دار شود (برای token10)
.env + config/services.yaml افزودن KAVENEGAR_OTP_TEMPLATE و bind به OtpService
docs/api/sms.md مستندسازی سوییچ OTP به Lookup

وضعیت فعلی

OtpService::sendCode (متن‌آزاد، بدون templateCode):

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);
}

Constructor فعلی:

public function __construct(
    private readonly CacheInterface  $cache,
    private readonly SmsService      $sms,
    private readonly SmsTextResolver $smsText,
    private readonly CityRepository  $cityRepo,
    private readonly int             $otpTtl = 1200,
    private readonly string          $appEnv = 'dev',
) {}

SmsService::dispatchAsync / sendNow (مسیر انتخاب send vs lookup):

public function dispatchAsync(
    string $mobile, string $message, string $provider = 'kavenegar',
    ?string $templateUuid = null, array $templateVars = [],
    ?string $templateCode = null, string $tag = SmsLog::TAG_GLOBAL,
): void { /* dispatch SendSmsMessage */ }

// sendNow:
$success = ($msg->templateCode !== null)
    ? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
    : $provider->send($msg->mobile, $msg->message);

KavehNegarProvider::sendTemplate (map ترتیبی فعلی — فاصله را در token2/3 می‌گذارد که رد می‌شود):

public function sendTemplate(string $mobile, string $templateCode, array $vars): bool
{
    try {
        $params = ['receptor' => $mobile, 'template' => $templateCode];
        foreach (array_values($vars) as $i => $v) {
            $params['token' . ($i > 0 ? $i + 1 : '')] = $v;
        }
        $resp = $this->httpClient->request('POST',
            self::BASE . '/' . $this->key() . '/verify/lookup.json', [
                'body' => http_build_query($params), 'timeout' => 10,
            ]);
        $data = $resp->toArray();
        return ($data['return']['status'] ?? 0) === 200;
    } catch (\Throwable $e) { /* log; return false */ }
}

caller دیگر sendTemplate: فقط POST /api/v1/sms/send-template در src/Sms/Controller/SmsController.php:488 که $vars را با کلیدهای معنایی (مثل name,date) و وابسته به ترتیب پاس می‌دهد. پس map ترتیبی نباید برای این caller بشکند.

.env فعلی:

OTP_TTL=1200
KAVENEGAR_API_KEY=change_me

وظایف

۱. KavehNegarProvider::sendTemplate — پشتیبانی توکنِ نام‌دار (backward-safe)

اگر همه‌ی کلیدهای $vars نام slot معتبر کاوه‌نگار باشند (token,token2,token3,token10,token20) → همان‌ها را مستقیم استفاده کن (تا اسم سایتِ فاصله‌دار در token10 برود). در غیر این صورت (کلیدهای معنایی/لیستی) → همان map ترتیبی فعلی (برای caller send-template).

$params = ['receptor' => $mobile, 'template' => $templateCode];
$slots = ['token', 'token2', 'token3', 'token10', 'token20'];
if ($vars !== [] && array_keys($vars) !== range(0, count($vars) - 1)
    && array_diff(array_keys($vars), $slots) === []) {
    foreach ($vars as $slot => $v) { $params[$slot] = $v; }
} else {
    foreach (array_values($vars) as $i => $v) {
        $params['token' . ($i > 0 ? $i + 1 : '')] = $v;
    }
}

۲. .env + config/services.yaml — نام پترن OTP

.env (کاربر مقدار واقعی پترن مصوب را می‌گذارد):

KAVENEGAR_OTP_TEMPLATE=

config/services.yaml روی سرویس OtpService:

App\Auth\Service\OtpService:
    arguments:
        $otpTtl: '%env(int:OTP_TTL)%'
        $appEnv: '%kernel.environment%'
        $otpTemplate: '%env(default::KAVENEGAR_OTP_TEMPLATE)%'

۳. OtpService — dispatch از طریق Lookup

  • پارامتر constructor جدید ?string $otpTemplate = null بعد از پارامترهای بدون‌دیفالت و کنار $appEnv (ترتیب معتبر PHP؛ همه دیفالت‌دار در انتها).
  • در sendCode، وقتی پترن ست است → با templateCode و توکن‌های نام‌دار dispatch کن:
if ($this->appEnv !== 'dev') {
    $site    = $this->resolveSiteName($domain);
    $message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]); // فقط برای لاگ SmsLog

    if ($this->otpTemplate) {
        $this->sms->dispatchAsync(
            $mobile, $message,
            templateCode: $this->otpTemplate,
            templateVars: ['token' => $code, 'token10' => $site],
            tag: SmsLog::TAG_OTP,
        );
    } else {
        // fallback متن‌آزاد فقط وقتی پترن ست نشده (مثلاً محیط توسعه)
        $this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
    }
}
  • token = کد ۵ رقمی (بدون فاصله ✓). token10 = اسم سایت (تا ۵ فاصله مجاز ✓).
  • اگر پترن مصوب فقط %token داشته باشد، token10 اضافی توسط کاوه‌نگار نادیده گرفته می‌شود (بی‌خطر) — اسم سایت فقط وقتی نمایش داده می‌شود که پترن %token10 هم داشته باشد.

۴. مستندسازی docs/api/sms.md

  • در بخش «متن ویرایش‌پذیر پیامک‌های سیستمی»، تگ otp: تصریح کن که ارسال OTP از طریق Kavenegar VerifyLookup انجام می‌شود (نه send متن‌آزاد) وقتی KAVENEGAR_OTP_TEMPLATE ست باشد؛ متنِ پیام از پترن مصوب کاوه‌نگار می‌آید نه از body قابل‌ویرایش DB (body صرفاً برای لاگ SmsLog).
  • map توکن‌ها را مستند کن: token = کد، token10 = اسم سایت (فاصله‌دار).
  • الزام .env: KAVENEGAR_OTP_TEMPLATE باید نام پترن مصوب باشد؛ بدون آن، fallback به متن‌آزاد.

نکات مهم

  • فقط OTP به Lookup می‌رود؛ مسیر send() متن‌آزاد برای بقیه‌ی تگ‌ها و caller send-template دست‌نخورده بماند.
  • قانون فاصله رعایت شود: کد → token؛ هر مقدار فاصله‌دار (اسم سایت) → token10. هرگز اسم فاصله‌دار در token/token2/token3 نگذار.
  • پترن باید در پنل کاوه‌نگار مصوب باشد و ساختار توکنش با map کد بخواند (%token برای کد، در صورت نیاز %token10 برای سایت). این خارج از کد است و باید توسط صاحب حساب انجام شود.
  • نیازمند اشتراک advanced کاوه‌نگار.
  • backward-safe: تغییر sendTemplate نباید caller send-template (کلیدهای معنایی/ترتیبی) را بشکند — با شرط «همه کلیدها slot معتبرند» تضمین شود.
  • cache: بعد از تغییر constructor OtpService و bind جدید، cache:clear برای هر دو محیط dev و test لازم است (وگرنه کانتینر کامپایل‌شده‌ی قدیمی TypeError می‌دهد). worker پیامک (messenger:consume async) هم باید ری‌استارت شود.
  • تست:
    ddev exec php -l src/Auth/Service/OtpService.php
    ddev exec php -l src/Sms/Provider/KavehNegarProvider.php
    ddev exec php bin/console cache:clear
    ddev exec php bin/console cache:clear --env=test
    ddev exec php bin/console lint:container
    ddev exec php bin/phpunit tests/Auth/SendCodeMobileRateLimitTest.php
    
    (تست واقعی ارسال Lookup نیاز به API key و پترن مصوب دارد؛ در dev/test پیامک ارسال نمی‌شود.)
  • بعد از تغییر، docs/api/sms.md را در همان session به‌روز کن (قانون استاندینگ پروژه).