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.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# ارسال پیامک 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):
|
||||
|
||||
```php
|
||||
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 فعلی:
|
||||
|
||||
```php
|
||||
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):
|
||||
|
||||
```php
|
||||
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 میگذارد که رد میشود):
|
||||
|
||||
```php
|
||||
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`).
|
||||
|
||||
```php
|
||||
$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`:
|
||||
```yaml
|
||||
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 کن:
|
||||
|
||||
```php
|
||||
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`) هم باید ریاستارت شود.
|
||||
- **تست**:
|
||||
```bash
|
||||
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 بهروز کن (قانون استاندینگ پروژه).
|
||||
Reference in New Issue
Block a user