Files

93 lines
8.9 KiB
Markdown
Raw Permalink Normal View History

# طرح افزودن درگاه پیامکی جدید (Relay / `createsms`)
> وضعیت: **اجرا شد** (2026-10-05) — تصمیم ثبت‌شده: D26.
> تاریخ: 2026-10-05
## ۱. کد ورودی چه می‌کند
کد PHP ارسالی (Laravel) یک پیامک را به یک سرویس واسط (relay) می‌فرستد:
| مورد | مقدار |
|---|---|
| متد / آدرس | `POST http://185.231.112.101/api/createsms` (HTTP ساده، بدون TLS) |
| بدنه | JSON |
| هدر احراز هویت | `secret_key: <کلید>` |
| timeout | ۱۵ ثانیه |
| فیلد `phone` | شماره گیرنده |
| فیلد `text` | متن پیام، `trim()` شده |
| فیلد `host` | `Controller::Config('montek')` و اگر خالی بود IP خود سرور (`gethostbyname(gethostname())`) — یعنی شناسه‌ی «این مشتری/سرور» نزد سرویس واسط |
| فیلد `platform` | اگر `$sendFromKave` درست باشد `"kave"` (کاوه‌نگار)، وگرنه `null` (خط پیش‌فرض سرویس واسط) |
| `CURLOPT_IPRESOLVE = V4` | فقط اجبار IPv4؛ چون آدرس خودش IP است در عمل اثری ندارد |
این کد در `henza_server` وجود ندارد (جستجو شد)، پس از سیستم دیگری آمده و مرجع parity ندارد.
## ۲. کجا قرار می‌گیرد
ماژول پیامک همین الان در [apps/api/src/modules/sms/sms.service.ts](../apps/api/src/modules/sms/sms.service.ts) هست و همه‌ی مصرف‌کننده‌ها (OTP ورود، پیامک خوش‌آمد، اعلان تکمیل پروفایل، …) فقط `SmsService.send()` را صدا می‌زنند. پس:
- **هیچ مصرف‌کننده‌ای تغییر نمی‌کند.** فقط یک «درایور» دوم کنار Trez اضافه می‌شود و با `SMS_PROVIDER=relay` انتخاب می‌شود.
- قوانین فعلی حفظ می‌شود: `send()` هرگز throw نمی‌کند (D20)، هر پیام در `SmsMessage` ثبت می‌شود، متن OTP ذخیره نمی‌شود (SEC-11)، نبود تنظیمات فقط هشدار می‌دهد و سرور بالا می‌آید.
## ۳. تغییرات پیشنهادی
### ۳.۱ کد — `sms.service.ts`
1. `smsConfig()` متغیرهای جدید را هم می‌خواند (جدول ۳.۲).
2. `smsConfigured()` برای `relay`: کافی است `SMS_RELAY_SECRET` و `SMS_RELAY_HOST` پر باشند.
3. `assertSmsConfigured()` مقدار `relay` را می‌شناسد و متغیرهای خالی را با نام گزارش می‌کند (بدون throw).
4. متد جدید `deliverViaRelay(mobile, text, platform)`:
- `fetch` با `Content-Type: application/json` و هدر `secret_key`، بدنه‌ی `{ phone, host, text, platform }`، timeout ۱۵ ثانیه با `AbortController` — هم‌شکل `deliverViaTrez`.
- موفقیت = پاسخ HTTP 2xx (تا وقتی قالب پاسخ سرویس واسط مشخص شود — سؤال ۴).
5. `SmsSendOptions` یک گزینه‌ی اختیاری `platform?: 'kave'` می‌گیرد؛ درایور Trez آن را نادیده می‌گیرد.
6. اجبار IPv4 پیاده نمی‌شود چون آدرس IP است؛ اگر بعداً دامنه شد، با `dns.lookup` با `family: 4` اضافه می‌شود.
### ۳.۲ متغیرهای محیطی جدید
| متغیر | اجباری | توضیح |
|---|---|---|
| `SMS_PROVIDER` | — | مقدار جدید `relay` |
| `SMS_RELAY_URL` | خیر | پیش‌فرض `http://185.231.112.101/api/createsms` |
| `SMS_RELAY_SECRET` | **بله** | مقدار هدر `secret_key` — **فقط در env، هرگز در کد یا git** |
| `SMS_RELAY_HOST` | **بله** | مقدار فیلد `host` (معادل `Config('montek')`) |
| `SMS_RELAY_KAVE_FOR` | خیر | کدام پیام‌ها از کاوه‌نگار بروند — بسته به پاسخ سؤال ۲ |
### ۳.۳ مستندات و استقرار
- `.env.example` (ریشه)، `apps/api/.env.example`، `docker-compose.yml`، `DELIVERY.md` (بخش SMS و گام ۴): افزودن متغیرهای بالا.
- `docs/parity/DECISIONS.md`: ثبت یک تصمیم جدید (D26) برای درایور relay.
### ۳.۴ تست — `sms.service.spec.ts`
- تنظیمات: پیش‌فرض URL، `smsConfigured` با/بدون secret و host، هشدار با نام متغیرهای خالی.
- ارسال با `fetch` ماک‌شده: آدرس، هدر `secret_key`، بدنه‌ی JSON، `trim` متن، `platform` درست؛ و این‌که خطای شبکه/timeout/HTTP 500 به `{ ok: false }` تبدیل شود و throw نشود.
## ۴. هشدار امنیتی ⚠
1. **کلید `secret_key` لو رفته فرض شود.** در متن پیام چت آمده و احتمالاً در git پروژه‌ی PHP هم هست (مشابه SEC-12). باید از صاحب سرویس واسط کلید جدید گرفت و فقط در env سرور گذاشت. این کلید در کد این مخزن نوشته نمی‌شود.
2. **ارتباط HTTP ساده است**؛ کلید و متن پیام (از جمله کد OTP) رمزنشده از اینترنت عبور می‌کنند. اگر سرویس واسط HTTPS دارد از آن استفاده شود.
## ۵. سؤال‌ها (با پیشنهاد)
1. **جایگزین یا کنار Trez؟** پیشنهاد: کنار Trez بماند و با `SMS_PROVIDER=relay` انتخاب شود؛ پیش‌فرض همان `trez` بماند تا رفتار فعلی عوض نشود. یا پیش‌فرض `relay` شود؟
2. **`sendFromKave` کی درست است؟** در کد اصلی این متغیر از کجا مقدار می‌گیرد؟ پیشنهاد: فقط OTP ورود از کاوه‌نگار برود (معمولاً برای سرعت/تحویل بهتر) و بقیه از خط پیش‌فرض — قابل تنظیم با env.
3. **مقدار `host` (`montek`) چیست؟** پیشنهاد: اجباری در env. fallback به IP خود سرور پیاده نمی‌شود، چون داخل Docker، IP کانتینر (مثلاً `172.x`) برمی‌گردد که برای سرویس واسط بی‌معناست.
4. **پاسخ سرویس واسط چه شکلی است؟** اگر در خطا هم 200 برمی‌گرداند (مثلاً `{"status": false}`)، نمونه‌ی پاسخ موفق و ناموفق لازم است تا `ok` درست تشخیص داده شود. پیشنهاد فعلی: فقط وضعیت HTTP.
5. **HTTPS دارد؟** (بند ۴.۲)
## ۷. پاسخ‌های مالک (2026-10-05)
| سؤال | پاسخ | اثر روی پیاده‌سازی |
|---|---|---|
| کلید | لو نرفته؛ تأیید دومرحله‌ای جای دیگری هست؛ داخل پروژه قرار بگیرد | کلید به‌عنوان مقدار پیش‌فرض در کد می‌آید و `SMS_RELAY_SECRET` فقط آن را بازنویسی می‌کند. بند ۴.۱ لغو شد. |
| ۱. جایگزین Trez؟ | پیش‌فرض `relay` شود؛ Trez حذف نشود؛ ممکن است بعداً سرویس‌های بیشتری بیاید | سرویس به ساختار «درایور» بازنویسی می‌شود: یک اینترفیس `SmsDriver` و هر سرویس یک فایل جدا (`drivers/relay.driver.ts`، `drivers/trez.driver.ts`). برای اضافه کردن سرویس بعدی فقط یک فایل و یک خط ثبت لازم است. پیش‌فرض `SMS_PROVIDER` برابر `relay` می‌شود. |
| ۳. `host` | IP سرور درست است | `SMS_RELAY_HOST` اختیاری است. اگر خالی باشد، مثل PHP اولین IPv4 غیرداخلی سرور فرستاده می‌شود. |
| ۴. پاسخ | با تشخیص خودم | `ok` فقط وقتی است که HTTP 2xx باشد و بدنه‌ی JSON هم نشانه‌ی شکست صریح نداشته باشد (`status: false`، `success: false`، `error`). در غیر این صورت `ok: false` + لاگ؛ هیچ‌وقت throw نمی‌شود. |
| ۵. HTTPS | دارد | بررسی شد: روی `https://185.231.112.101` گواهی `*.montpar.ir` نصب است که **از 2026-08-22 منقضی شده** و روی IP هم معتبر نیست ← سؤال ب |
### سؤال‌های باز
- **الف) `sendFromKave`:** کدام پیام‌ها از کاوه‌نگار بروند؟ پیشنهاد: فقط OTP ورود.
- **ب) آدرس HTTPS:** نام دامنه‌ی سرویس (زیرمجموعه‌ای از `montpar.ir`) چیست؟ گواهی هم باید تمدید شود. تا آن موقع پیش‌فرض همان `http://185.231.112.101/api/createsms` می‌ماند و با `SMS_RELAY_URL` عوض می‌شود.
## ۶. وضعیت اجرا
- [x] تأیید طرح و پاسخ سؤال‌ها (الف: همه‌ی پیام‌ها از کاوه‌نگار؛ ب: همان IP و HTTP درست است)
- [x] کد + تست (`apps/api/src/modules/sms/drivers/`، ۱۳۵ تست سبز)
- [x] env / compose / DELIVERY / DECISIONS (D26) / ISSUES (REL-02)