feat(sms): simplify SMS settings to use only KAVENEGAR_API_KEY from env, remove other SMS-related fields from config
This commit is contained in:
@@ -0,0 +1,311 @@
|
|||||||
|
# سادهسازی «تنظیمات پیامک (SMS)» — فقط API Key از env
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`clinicpro` (Backend Symfony + پنل ادمین React)
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
در پنل ادمین (`https://clinic-pro.ddev.site/admin` → صفحه تنظیمات) بخش **«تنظیمات پیامک (SMS)»** الان چهار مورد قابلویرایش دارد که همه در جدول DB `site_config` ذخیره میشوند:
|
||||||
|
|
||||||
|
- `sms_provider` (انتخاب سرویس: کاوهنگار / رنگینه)
|
||||||
|
- `sms_price_rials` (قیمت هر پیامک)
|
||||||
|
- `kavenegar_api_key` (کلید API)
|
||||||
|
- `kavenegar_sender` (شماره فرستنده)
|
||||||
|
|
||||||
|
مقدار API Key عملاً باید یک راز (secret) باشد و جای آن env است، نه دیتابیس یا فرم قابلویرایش پنل. تصمیم نهایی:
|
||||||
|
|
||||||
|
- **تنها `KAVENEGAR_API_KEY` در env بماند.** بقیه env های SMS حذف شوند.
|
||||||
|
- در پنل، کلید فقط **read-only** نمایش داده شود (وضعیت «تنظیمشده / تنظیمنشده») و از env خوانده شود؛ دیگر در DB ذخیره نشود.
|
||||||
|
- سه مورد دیگر (provider، price، sender) از **UI و Backend بهطور کامل حذف** شوند.
|
||||||
|
- خودِ کارت/بخش «تنظیمات پیامک (SMS)» باقی بماند.
|
||||||
|
|
||||||
|
## هدف
|
||||||
|
|
||||||
|
1. API Key فقط از env (`KAVENEGAR_API_KEY`) — read-only در پنل، بدون ذخیره در DB.
|
||||||
|
2. حذف کامل `sms_provider`، `sms_price_rials`، `kavenegar_sender` (و کلیدهای rangineh) از UI و Backend config.
|
||||||
|
3. Provider همیشه `kavenegar`. Sender خالی → کاوهنگار از خط پیشفرض حساب استفاده میکند.
|
||||||
|
4. `sms_price_rials` که قبلاً از config خوانده میشد، تبدیل به ثابت (constant) شود تا کیفپول پیامک نشکند.
|
||||||
|
5. همه فایلهای env بهروزرسانی شوند: فقط `KAVENEGAR_API_KEY` بماند.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش | تغییر |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `assets/admin/pages/SettingsPage.tsx` | فرم تنظیمات پنل | حذف ۳ فیلد، تبدیل API Key به read-only status |
|
||||||
|
| `assets/admin/types/index.ts` | تایپها | حذف `sms_price_rials` |
|
||||||
|
| `src/Config/Controller/SiteConfigController.php` | endpoint `/api/v1/admin/settings` | حذف کلیدهای SMS از `ALLOWED_KEYS`؛ افزودن flag read-only |
|
||||||
|
| `src/Config/Repository/SiteConfigRepository.php` | مقادیر پیشفرض config | حذف کلیدهای SMS از `DEFAULTS` |
|
||||||
|
| `src/Sms/Provider/KavehNegarProvider.php` | provider کاوهنگار | خواندن key/sender فقط از env، نه configRepo |
|
||||||
|
| `src/Sms/Controller/SmsWalletController.php` | balance/price | `sms_price_rials` از ثابت بهجای config |
|
||||||
|
| `config/services.yaml` | wiring provider | تنظیم binding های env |
|
||||||
|
| `.env` و بقیه `.env.*` | متغیرهای محیطی | فقط `KAVENEGAR_API_KEY` |
|
||||||
|
| `docs/api/sms.md` | مستندات | ذکر منبع جدید API Key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وضعیت فعلی (کد واقعی)
|
||||||
|
|
||||||
|
### ۱. فرم پنل — `assets/admin/pages/SettingsPage.tsx`
|
||||||
|
|
||||||
|
Zod schema (خطوط ۴۰–۴۳):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
sms_provider: z.string(),
|
||||||
|
kavenegar_api_key: z.string(),
|
||||||
|
kavenegar_sender: z.string(),
|
||||||
|
sms_price_rials: z.string(),
|
||||||
|
```
|
||||||
|
|
||||||
|
interface (خطوط ۶۵–۶۸) + reset defaults (۱۲۳–۱۲۶) + reset onClick (۵۴۴–۵۴۷) هم همین ۴ کلید را دارند.
|
||||||
|
|
||||||
|
بخش UI (خطوط ۴۷۱–۵۲۰):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{/* تنظیمات پیامک */}
|
||||||
|
<div className="card card-pad">
|
||||||
|
{/* ... هدر «تنظیمات پیامک (SMS)» ... */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label>سرویس پیامک</label>
|
||||||
|
<select {...register('sms_provider')}>
|
||||||
|
<option value="kavenegar">کاوهنگار</option>
|
||||||
|
<option value="rangineh">رنگینه</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>قیمت هر پیامک (ریال)</label>
|
||||||
|
<input {...register('sms_price_rials')} type="number" ... />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
<div>تنظیمات کاوهنگار</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
||||||
|
<div>
|
||||||
|
<label>API Key</label>
|
||||||
|
<input {...register('kavenegar_api_key')} type={showKavenegarKey ? 'text' : 'password'} ... />
|
||||||
|
{/* دکمه نمایش/پنهان */}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>شماره فرستنده</label>
|
||||||
|
<input {...register('kavenegar_sender')} ... />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۲. `src/Config/Repository/SiteConfigRepository.php` — `DEFAULTS` (خطوط ۳۲–۳۷)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// sms provider
|
||||||
|
'sms_provider' => 'kavenegar',
|
||||||
|
'kavenegar_api_key' => '',
|
||||||
|
'kavenegar_sender' => '',
|
||||||
|
'rangineh_api_key' => '',
|
||||||
|
'rangineh_sender' => '',
|
||||||
|
'sms_price_rials' => '500',
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۳. `src/Config/Controller/SiteConfigController.php` — `ALLOWED_KEYS` (خطوط ۳۸–۴۴)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// sms provider
|
||||||
|
'sms_provider',
|
||||||
|
'kavenegar_api_key',
|
||||||
|
'kavenegar_sender',
|
||||||
|
'rangineh_api_key',
|
||||||
|
'rangineh_sender',
|
||||||
|
'sms_price_rials',
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۴. `src/Sms/Provider/KavehNegarProvider.php` (خطوط ۱۳–۲۴)
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly SiteConfigRepository $configRepo,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly ?string $apiKey = null,
|
||||||
|
private readonly ?string $sender = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function key(): string { return $this->configRepo->get('kavenegar_api_key') ?: ($this->apiKey ?? ''); }
|
||||||
|
private function sender(): string { return $this->configRepo->get('kavenegar_sender') ?: ($this->sender ?? ''); }
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۵. `src/Sms/Controller/SmsWalletController.php` (خط ۵۶)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$smsPriceRials = (int) ($this->configRepo->get('sms_price_rials') ?? 500);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۶. `config/services.yaml` (خطوط ۹۲–۹۹)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
App\Sms\Provider\KavehNegarProvider:
|
||||||
|
arguments:
|
||||||
|
$apiKey: '%env(default::KAVENEGAR_API_KEY)%'
|
||||||
|
$sender: '%env(default::KAVENEGAR_SENDER)%'
|
||||||
|
|
||||||
|
App\Sms\Provider\RanginehProvider:
|
||||||
|
arguments:
|
||||||
|
$apiKey: '%env(default::RANGINEH_API_KEY)%'
|
||||||
|
$sender: '%env(default::RANGINEH_SENDER)%'
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۷. env فعلی — `.env` (خطوط ۳۸–۴۲ و ۶۰–۶۴)
|
||||||
|
|
||||||
|
```
|
||||||
|
###> SMS ###
|
||||||
|
KAVENEGAR_API_KEY=change_me
|
||||||
|
RANGINEH_API_KEY=change_me
|
||||||
|
SMS_PROVIDER=kavenegar
|
||||||
|
###< SMS ###
|
||||||
|
...
|
||||||
|
# SMS
|
||||||
|
KAVENEGAR_API_KEY=test_key
|
||||||
|
KAVENEGAR_SENDER=1000596446
|
||||||
|
RANGINEH_API_KEY=test_key
|
||||||
|
RANGINEH_SENDER=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
> توجه: `.env` هم بلوک `###> SMS ###` دارد هم یک بخش دوم پایین فایل. هر دو باید یکی شوند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. Backend — provider کاوهنگار فقط از env
|
||||||
|
|
||||||
|
در `src/Sms/Provider/KavehNegarProvider.php`:
|
||||||
|
|
||||||
|
- متد `key()` و `sender()` دیگر از `configRepo` نخوانند؛ فقط از env (constructor):
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function key(): string { return $this->apiKey ?? ''; }
|
||||||
|
private function sender(): string { return $this->sender ?? ''; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- چون `SiteConfigRepository` دیگر در این کلاس استفاده نمیشود، `use App\Config\Repository\SiteConfigRepository;` و پارامتر `private readonly SiteConfigRepository $configRepo,` از constructor حذف شود.
|
||||||
|
- کامنت خطوط ۱۷–۱۸ («the real key normally comes from DB site config») هم اصلاح شود که حالا فقط از env میآید.
|
||||||
|
|
||||||
|
### ۲. Backend — حذف کلیدهای SMS از config
|
||||||
|
|
||||||
|
- `src/Config/Repository/SiteConfigRepository.php`: کل بلوک `// sms provider` (۶ کلید: `sms_provider`, `kavenegar_api_key`, `kavenegar_sender`, `rangineh_api_key`, `rangineh_sender`, `sms_price_rials`) از `DEFAULTS` حذف شود.
|
||||||
|
- `src/Config/Controller/SiteConfigController.php`: همان ۶ کلید از `ALLOWED_KEYS` حذف شوند (دیگر از طریق PATCH قابل ذخیره نیستند).
|
||||||
|
|
||||||
|
### ۳. Backend — قیمت پیامک به ثابت
|
||||||
|
|
||||||
|
در `src/Sms/Controller/SmsWalletController.php`، چون `sms_price_rials` دیگر در config نیست، آن را ثابت کن:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class SmsWalletController extends BaseController
|
||||||
|
{
|
||||||
|
private const SMS_PRICE_RIALS = 500;
|
||||||
|
...
|
||||||
|
// بهجای config:
|
||||||
|
$smsPriceRials = self::SMS_PRICE_RIALS;
|
||||||
|
```
|
||||||
|
|
||||||
|
> `SmsWalletPage.tsx` (خط ۱۲۶) مقدار `balance.sms_price_rials` را از پاسخ همین endpoint میخواند؛ چون همچنان در پاسخ برگردانده میشود، نیازی به تغییر ندارد.
|
||||||
|
|
||||||
|
### ۴. Backend — نمایش وضعیت read-only کلید در پنل
|
||||||
|
|
||||||
|
چون API Key دیگر در DB نیست، برای اینکه پنل بتواند «تنظیمشده/نشده» را نشان دهد، در `SiteConfigController::get()` یک flag محاسبهشده اضافه کن (بدون افشای خودِ کلید):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/api/v1/admin/settings', methods: ['GET'])]
|
||||||
|
public function get(): JsonResponse
|
||||||
|
{
|
||||||
|
$config = $this->configRepo->getAll();
|
||||||
|
// API Key پیامک فقط از env؛ فقط وضعیت تنظیمبودن برگردانده میشود نه مقدار.
|
||||||
|
$config['sms_api_key_configured'] = ($_ENV['KAVENEGAR_API_KEY'] ?? '') !== '';
|
||||||
|
return $this->success($config);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> اگر پروژه برای خواندن env از سرویس/پارامتر خاصی استفاده میکند، از همان الگو استفاده کن؛ اگر نه، خواندن مستقیم از `$_ENV` قابلقبول است.
|
||||||
|
|
||||||
|
### ۵. `config/services.yaml`
|
||||||
|
|
||||||
|
- برای `KavehNegarProvider` فقط `$apiKey` از env بماند؛ خط `$sender` حذف شود (constructor مقدار پیشفرض `null` دارد → خط پیشفرض کاوهنگار):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
App\Sms\Provider\KavehNegarProvider:
|
||||||
|
arguments:
|
||||||
|
$apiKey: '%env(default::KAVENEGAR_API_KEY)%'
|
||||||
|
```
|
||||||
|
|
||||||
|
- binding های `RanginehProvider` (خطوط ۹۶–۹۹) که به env حذفشده اشاره میکنند حذف شوند. کلاس `RanginehProvider` و تزریق آن در `SmsService` دستنخورده بماند (فقط دیگر انتخاب نمیشود؛ `resolveProvider()` بهصورت پیشفرض `kavenegar` برمیگرداند).
|
||||||
|
|
||||||
|
### ۶. Frontend — `assets/admin/pages/SettingsPage.tsx`
|
||||||
|
|
||||||
|
- از Zod schema (۴۰–۴۳): حذف `sms_provider`، `kavenegar_sender`، `sms_price_rials`. کلید `kavenegar_api_key` هم چون دیگر ارسال نمیشود از schema حذف شود.
|
||||||
|
- از interface تنظیمات (۶۵–۶۸)، reset defaults (۱۲۳–۱۲۶) و reset onClick (۵۴۴–۵۴۷): همان کلیدها حذف شوند.
|
||||||
|
- state مربوط به `showKavenegarKey` حذف شود (دیگر input رمزی نداریم).
|
||||||
|
- کارت «تنظیمات پیامک (SMS)» بماند اما محتوایش به یک نمایش read-only تبدیل شود که وضعیت را از `settings.sms_api_key_configured` میخواند:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{/* تنظیمات پیامک */}
|
||||||
|
<div className="card card-pad">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: '1.25rem' }}>
|
||||||
|
<div className="ico" style={{ background: 'var(--violet-bg)', color: 'var(--violet)', width: 36, height: 36, borderRadius: 10 }}>
|
||||||
|
<span style={{ fontSize: 16 }}>📱</span>
|
||||||
|
</div>
|
||||||
|
<h3 style={{ fontSize: 15 }}>تنظیمات پیامک (SMS)</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>
|
||||||
|
کلید API کاوهنگار
|
||||||
|
</label>
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--muted)' }}>
|
||||||
|
{settings?.sms_api_key_configured
|
||||||
|
? <span style={{ color: 'var(--success)' }}>✓ تنظیمشده (از متغیر محیطی KAVENEGAR_API_KEY)</span>
|
||||||
|
: <span style={{ color: 'var(--danger)' }}>✗ تنظیمنشده — مقدار KAVENEGAR_API_KEY را در فایل env قرار دهید</span>}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 8 }}>
|
||||||
|
کلید API فقط از طریق متغیر محیطی سرور مدیریت میشود و در این پنل قابل ویرایش نیست.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `assets/admin/types/index.ts` خط ۴۱۵: `sms_price_rials: number;` حذف شود. اگر تایپ تنظیمات، `sms_provider`/`kavenegar_api_key`/`kavenegar_sender` هم دارد، آنها هم حذف و در صورت لزوم `sms_api_key_configured: boolean;` اضافه شود.
|
||||||
|
|
||||||
|
### ۷. بهروزرسانی همه فایلهای env
|
||||||
|
|
||||||
|
در **همه** این فایلها فقط `KAVENEGAR_API_KEY` بماند و `KAVENEGAR_SENDER`، `RANGINEH_API_KEY`، `RANGINEH_SENDER`، `SMS_PROVIDER` حذف شوند:
|
||||||
|
|
||||||
|
`.env`, `.env.example`, `.env.dev`, `.env.local`, `.env.test`, `.env.coolify.example`, `.env.liara.example`
|
||||||
|
|
||||||
|
الگوی نهایی بلوک SMS (در `.env`):
|
||||||
|
|
||||||
|
```
|
||||||
|
###> SMS ###
|
||||||
|
KAVENEGAR_API_KEY=change_me
|
||||||
|
###< SMS ###
|
||||||
|
```
|
||||||
|
|
||||||
|
- در `.env` بخش تکراری پایین فایل (خطوط ۶۰–۶۴) هم پاکسازی شود؛ فقط یک `KAVENEGAR_API_KEY` باقی بماند.
|
||||||
|
- در `.env.example` کامنت خط ۴۱ اصلاح شود به این مضمون: «کلید کاوهنگار فقط از env خوانده میشود» (نه از DB).
|
||||||
|
- برای فایلهای نمونه (`*.example`) مقدار placeholder مثل `KAVENEGAR_API_KEY=` یا `change_me` بگذار؛ در `.env.test` مقدار تستی (مثلاً `test_key`).
|
||||||
|
|
||||||
|
### ۸. مستندات — `docs/api/sms.md`
|
||||||
|
|
||||||
|
- ذکر شود API Key کاوهنگار فقط از env `KAVENEGAR_API_KEY` خوانده میشود.
|
||||||
|
- ذکر شود endpoint `/api/v1/admin/settings` دیگر کلیدهای `sms_provider`، `kavenegar_api_key`، `kavenegar_sender`، `rangineh_*`، `sms_price_rials` را نمیپذیرد؛ در پاسخ GET فیلد read-only `sms_api_key_configured` اضافه شده.
|
||||||
|
- provider ثابت `kavenegar` و قیمت پیامک ثابت `SMS_PRICE_RIALS = 500` مستند شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- **پاکسازی DB (اختیاری):** ردیفهای قدیمی این کلیدها ممکن است در جدول `site_config` مانده باشند. حذفشان ضروری نیست (دیگر خوانده نمیشوند) اما برای تمیزی میتوان با یک کوئری آنها را حذف کرد. migration لازم نیست چون Entity تغییر نکرده.
|
||||||
|
- **Sender خالی:** با حذف `kavenegar_sender`، متد `send()` مقدار `sender=''` میفرستد؛ کاوهنگار در این حالت از خط پیشفرض حساب استفاده میکند. اگر خط اختصاصی لازم شد، بعداً میتوان `KAVENEGAR_SENDER` را به env و binding برگرداند — طبق درخواست فعلی فقط API Key در env میماند.
|
||||||
|
- **سازگاری پاسخ:** ساختار پاسخها با `$this->success()` حفظ شود (BaseController).
|
||||||
|
- **بررسی تایپ فرانت:** بعد از تغییر، `ddev exec npx tsc --noEmit --project tsconfig.json` خطا ندهد (کلیدهای حذفشده جایی رفرنس نشده باشند — خصوصاً `showKavenegarKey`).
|
||||||
|
- **بیلد:** `ddev exec yarn dev` برای تأیید بیلد JS/TS.
|
||||||
|
- **پاک کردن کش بعد از تغییر services.yaml/env:** `ddev exec php bin/console cache:clear`.
|
||||||
|
- **graphify:** بعد از پایان تغییرات `graphify update .` اجرا شود.
|
||||||
@@ -55,9 +55,12 @@ TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
|
|||||||
API_IR_BASE_URL=https://s.api.ir
|
API_IR_BASE_URL=https://s.api.ir
|
||||||
API_IR_TOKEN=
|
API_IR_TOKEN=
|
||||||
|
|
||||||
|
# ── SMS ──
|
||||||
|
# Kavenegar API key is read ONLY from this env (not the DB).
|
||||||
|
KAVENEGAR_API_KEY=
|
||||||
|
|
||||||
# ── Notes ──
|
# ── Notes ──
|
||||||
# • SMS keys (kavenegar/rangineh) and payment gateway keys (mellat/sep) are read
|
# • Payment gateway keys (mellat/sep) are read from the DB ("Site Settings"), NOT from env.
|
||||||
# from the DB ("Site Settings"), NOT from env. No need to set them here.
|
|
||||||
# • REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES are fixed in the compose file.
|
# • REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES are fixed in the compose file.
|
||||||
# • MariaDB & Redis are separate Coolify resources — see DATABASE_URL/REDIS_URL above.
|
# • MariaDB & Redis are separate Coolify resources — see DATABASE_URL/REDIS_URL above.
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -38,8 +38,10 @@ REFRESH_TOKEN_TTL=2592000
|
|||||||
OTP_TTL=1200
|
OTP_TTL=1200
|
||||||
###< Auth ###
|
###< Auth ###
|
||||||
|
|
||||||
# SMS keys (kavenegar/rangineh) از پنل «تنظیمات سایت» (DB) خوانده میشوند، نه env.
|
###> SMS ###
|
||||||
# env فقط fallback اختیاری است؛ نیازی به تعریف اینجا نیست.
|
# کلید API کاوهنگار فقط از همین env خوانده میشود (نه DB).
|
||||||
|
KAVENEGAR_API_KEY=
|
||||||
|
###< SMS ###
|
||||||
|
|
||||||
###> api.ir Identity Inquiry (Shahkar + IbanMatch) ###
|
###> api.ir Identity Inquiry (Shahkar + IbanMatch) ###
|
||||||
# استعلام هویت نماینده: تطبیق کد ملی با موبایل (شاهکار) و تطبیق شبا با کد ملی.
|
# استعلام هویت نماینده: تطبیق کد ملی با موبایل (شاهکار) و تطبیق شبا با کد ملی.
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ UPLOAD_DIR=var/uploads
|
|||||||
# ── Notes ──
|
# ── Notes ──
|
||||||
# • Do NOT set RUN_INIT — entrypoint.sh defaults it to 1, and this is a single app,
|
# • Do NOT set RUN_INIT — entrypoint.sh defaults it to 1, and this is a single app,
|
||||||
# so JWT keygen + doctrine migrations run once on boot (correct).
|
# so JWT keygen + doctrine migrations run once on boot (correct).
|
||||||
# • SMS keys (kavenegar/rangineh) and payment gateway keys (mellat/sep) are read
|
# • Kavenegar API key is read ONLY from env — set KAVENEGAR_API_KEY below.
|
||||||
# from the DB ("Site Settings"), NOT from env. No need to set them here.
|
# • Payment gateway keys (mellat/sep) are read from the DB ("Site Settings"), NOT from env.
|
||||||
# • Liara exposes only ONE external HTTP port (8080, from liara.json). MariaDB/Redis
|
# • Liara exposes only ONE external HTTP port (8080, from liara.json). MariaDB/Redis
|
||||||
# are reachable on the private network only.
|
# are reachable on the private network only.
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ const schema = z.object({
|
|||||||
mellat_username: z.string(),
|
mellat_username: z.string(),
|
||||||
mellat_password: z.string(),
|
mellat_password: z.string(),
|
||||||
sep_terminal_id: z.string(),
|
sep_terminal_id: z.string(),
|
||||||
// sms
|
|
||||||
sms_provider: z.string(),
|
|
||||||
kavenegar_api_key: z.string(),
|
|
||||||
kavenegar_sender: z.string(),
|
|
||||||
sms_price_rials: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
@@ -62,10 +57,7 @@ interface Settings {
|
|||||||
mellat_username: string;
|
mellat_username: string;
|
||||||
mellat_password: string;
|
mellat_password: string;
|
||||||
sep_terminal_id: string;
|
sep_terminal_id: string;
|
||||||
sms_provider: string;
|
sms_api_key_configured: boolean;
|
||||||
kavenegar_api_key: string;
|
|
||||||
kavenegar_sender: string;
|
|
||||||
sms_price_rials: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Component ─────────────────────────────────────────────────────────────
|
// ── Component ─────────────────────────────────────────────────────────────
|
||||||
@@ -73,7 +65,6 @@ interface Settings {
|
|||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [showMellatPassword, setShowMellatPassword] = useState(false);
|
const [showMellatPassword, setShowMellatPassword] = useState(false);
|
||||||
const [showKavenegarKey, setShowKavenegarKey] = useState(false);
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['admin-settings'],
|
queryKey: ['admin-settings'],
|
||||||
@@ -120,10 +111,6 @@ export default function SettingsPage() {
|
|||||||
mellat_username: settings.mellat_username ?? '',
|
mellat_username: settings.mellat_username ?? '',
|
||||||
mellat_password: settings.mellat_password ?? '',
|
mellat_password: settings.mellat_password ?? '',
|
||||||
sep_terminal_id: settings.sep_terminal_id ?? '',
|
sep_terminal_id: settings.sep_terminal_id ?? '',
|
||||||
sms_provider: settings.sms_provider ?? 'kavenegar',
|
|
||||||
kavenegar_api_key: settings.kavenegar_api_key ?? '',
|
|
||||||
kavenegar_sender: settings.kavenegar_sender ?? '',
|
|
||||||
sms_price_rials: settings.sms_price_rials ?? '500',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [settings, reset]);
|
}, [settings, reset]);
|
||||||
@@ -477,45 +464,16 @@ export default function SettingsPage() {
|
|||||||
<h3 style={{ fontSize: 15 }}>تنظیمات پیامک (SMS)</h3>
|
<h3 style={{ fontSize: 15 }}>تنظیمات پیامک (SMS)</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
<div>
|
||||||
<div>
|
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>کلید API کاوهنگار</label>
|
||||||
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>سرویس پیامک</label>
|
<div style={{ fontSize: 13 }}>
|
||||||
<select {...register('sms_provider')}
|
{settings?.sms_api_key_configured
|
||||||
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13 }}>
|
? <span style={{ color: 'var(--success)' }}>✓ تنظیمشده (از متغیر محیطی KAVENEGAR_API_KEY)</span>
|
||||||
<option value="kavenegar">کاوهنگار</option>
|
: <span style={{ color: 'var(--danger)' }}>✗ تنظیمنشده — مقدار KAVENEGAR_API_KEY را در فایل env قرار دهید</span>}
|
||||||
<option value="rangineh">رنگینه</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>قیمت هر پیامک (ریال)</label>
|
|
||||||
<input {...register('sms_price_rials')} type="number" min={0} dir="ltr" placeholder="500"
|
|
||||||
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '1rem' }}>
|
|
||||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: '0.75rem' }}>تنظیمات کاوهنگار</div>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
|
||||||
<div>
|
|
||||||
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>API Key</label>
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<input {...register('kavenegar_api_key')} type={showKavenegarKey ? 'text' : 'password'} dir="ltr" placeholder="••••••••••••••••"
|
|
||||||
style={{ width: '100%', height: 38, padding: '0 36px 0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={() => setShowKavenegarKey(p => !p)}
|
|
||||||
style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--muted)', fontSize: 12 }}>
|
|
||||||
{showKavenegarKey ? 'پنهان' : 'نمایش'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ fontSize: 12.5, fontWeight: 500, display: 'block', marginBottom: 4 }}>شماره فرستنده</label>
|
|
||||||
<input {...register('kavenegar_sender')} dir="ltr" placeholder="10008664"
|
|
||||||
style={{ width: '100%', height: 38, padding: '0 12px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, boxSizing: 'border-box' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 8 }}>
|
||||||
|
کلید API فقط از طریق متغیر محیطی سرور مدیریت میشود و در این پنل قابل ویرایش نیست.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -541,10 +499,6 @@ export default function SettingsPage() {
|
|||||||
mellat_username: settings.mellat_username,
|
mellat_username: settings.mellat_username,
|
||||||
mellat_password: settings.mellat_password,
|
mellat_password: settings.mellat_password,
|
||||||
sep_terminal_id: settings.sep_terminal_id,
|
sep_terminal_id: settings.sep_terminal_id,
|
||||||
sms_provider: settings.sms_provider,
|
|
||||||
kavenegar_api_key: settings.kavenegar_api_key,
|
|
||||||
kavenegar_sender: settings.kavenegar_sender,
|
|
||||||
sms_price_rials: settings.sms_price_rials,
|
|
||||||
})}
|
})}
|
||||||
disabled={!isDirty || mutation.isPending}
|
disabled={!isDirty || mutation.isPending}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -91,12 +91,6 @@ services:
|
|||||||
App\Sms\Provider\KavehNegarProvider:
|
App\Sms\Provider\KavehNegarProvider:
|
||||||
arguments:
|
arguments:
|
||||||
$apiKey: '%env(default::KAVENEGAR_API_KEY)%'
|
$apiKey: '%env(default::KAVENEGAR_API_KEY)%'
|
||||||
$sender: '%env(default::KAVENEGAR_SENDER)%'
|
|
||||||
|
|
||||||
App\Sms\Provider\RanginehProvider:
|
|
||||||
arguments:
|
|
||||||
$apiKey: '%env(default::RANGINEH_API_KEY)%'
|
|
||||||
$sender: '%env(default::RANGINEH_SENDER)%'
|
|
||||||
|
|
||||||
App\Blog\Controller\BlogController:
|
App\Blog\Controller\BlogController:
|
||||||
arguments:
|
arguments:
|
||||||
|
|||||||
+10
-2
@@ -1,9 +1,17 @@
|
|||||||
# SMS API
|
# SMS API
|
||||||
|
|
||||||
> **Prefix:** `/api/v1/sms`
|
> **Prefix:** `/api/v1/sms`
|
||||||
> **Providers:** `kavenegar` (default) | `rangineh`
|
> **Provider:** همیشه `kavenegar` (پیشفرض و تنها گزینه فعال).
|
||||||
> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue.
|
> All send operations are dispatched **asynchronously** via Symfony Messenger → Redis queue.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- **کلید API کاوهنگار فقط از متغیر محیطی `KAVENEGAR_API_KEY` خوانده میشود** — نه از دیتابیس و نه از پنل. در پنل ادمین فقط وضعیت read-only «تنظیمشده/نشده» نمایش داده میشود.
|
||||||
|
- شماره فرستنده تنظیم نمیشود؛ کاوهنگار از خط پیشفرض حساب استفاده میکند.
|
||||||
|
- endpoint `GET /api/v1/admin/settings` یک فیلد read-only به نام `sms_api_key_configured` (boolean) برمیگرداند.
|
||||||
|
- `PATCH /api/v1/admin/settings` دیگر کلیدهای `sms_provider`، `kavenegar_api_key`، `kavenegar_sender`، `rangineh_api_key`، `rangineh_sender`، `sms_price_rials` را نمیپذیرد (از `ALLOWED_KEYS` حذف شدهاند).
|
||||||
|
- قیمت هر پیامک ثابت است: `SmsWalletController::SMS_PRICE_RIALS = 500` ریال.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## POST `/api/v1/sms/send`
|
## POST `/api/v1/sms/send`
|
||||||
@@ -25,7 +33,7 @@ Send a direct SMS message (free text).
|
|||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `mobile` | string | ✅ | Recipient mobile (`09XXXXXXXXX`) |
|
| `mobile` | string | ✅ | Recipient mobile (`09XXXXXXXXX`) |
|
||||||
| `message` | string | ✅ | Message text |
|
| `message` | string | ✅ | Message text |
|
||||||
| `provider` | string | ❌ | `"kavenegar"` or `"rangineh"` (default from env) |
|
| `provider` | string | ❌ | نادیده گرفته میشود؛ همیشه `kavenegar` استفاده میشود |
|
||||||
|
|
||||||
### Response `200`
|
### Response `200`
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ class SiteConfigController extends BaseController
|
|||||||
'mellat_username',
|
'mellat_username',
|
||||||
'mellat_password',
|
'mellat_password',
|
||||||
'sep_terminal_id',
|
'sep_terminal_id',
|
||||||
// sms provider
|
|
||||||
'sms_provider',
|
|
||||||
'kavenegar_api_key',
|
|
||||||
'kavenegar_sender',
|
|
||||||
'rangineh_api_key',
|
|
||||||
'rangineh_sender',
|
|
||||||
'sms_price_rials',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -53,7 +46,10 @@ class SiteConfigController extends BaseController
|
|||||||
#[Route('/api/v1/admin/settings', methods: ['GET'])]
|
#[Route('/api/v1/admin/settings', methods: ['GET'])]
|
||||||
public function get(): JsonResponse
|
public function get(): JsonResponse
|
||||||
{
|
{
|
||||||
return $this->success($this->configRepo->getAll());
|
$config = $this->configRepo->getAll();
|
||||||
|
// کلید API پیامک فقط از env؛ فقط وضعیت تنظیمبودن برگردانده میشود نه مقدار.
|
||||||
|
$config['sms_api_key_configured'] = ($_ENV['KAVENEGAR_API_KEY'] ?? '') !== '';
|
||||||
|
return $this->success($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/v1/admin/settings', methods: ['PATCH'])]
|
#[Route('/api/v1/admin/settings', methods: ['PATCH'])]
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ class SiteConfigRepository extends ServiceEntityRepository
|
|||||||
'mellat_username' => '',
|
'mellat_username' => '',
|
||||||
'mellat_password' => '',
|
'mellat_password' => '',
|
||||||
'sep_terminal_id' => '',
|
'sep_terminal_id' => '',
|
||||||
// sms provider
|
|
||||||
'sms_provider' => 'kavenegar',
|
|
||||||
'kavenegar_api_key' => '',
|
|
||||||
'kavenegar_sender' => '',
|
|
||||||
'rangineh_api_key' => '',
|
|
||||||
'rangineh_sender' => '',
|
|
||||||
'sms_price_rials' => '500',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ use OpenApi\Attributes as OA;
|
|||||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||||
class SmsWalletController extends BaseController
|
class SmsWalletController extends BaseController
|
||||||
{
|
{
|
||||||
|
private const SMS_PRICE_RIALS = 500;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SmsWalletService $walletService,
|
private readonly SmsWalletService $walletService,
|
||||||
private readonly SmsWalletRepository $walletRepo,
|
private readonly SmsWalletRepository $walletRepo,
|
||||||
@@ -53,7 +55,7 @@ class SmsWalletController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$balanceRials = $this->walletService->getBalance($entityType, $entityId);
|
$balanceRials = $this->walletService->getBalance($entityType, $entityId);
|
||||||
$smsPriceRials = (int) ($this->configRepo->get('sms_price_rials') ?? 500);
|
$smsPriceRials = self::SMS_PRICE_RIALS;
|
||||||
$estimatedSms = $smsPriceRials > 0 ? (int) floor($balanceRials / $smsPriceRials) : 0;
|
$estimatedSms = $smsPriceRials > 0 ? (int) floor($balanceRials / $smsPriceRials) : 0;
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Sms\Provider;
|
namespace App\Sms\Provider;
|
||||||
|
|
||||||
use App\Config\Repository\SiteConfigRepository;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
@@ -12,16 +11,14 @@ class KavehNegarProvider implements SmsProviderInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HttpClientInterface $httpClient,
|
private readonly HttpClientInterface $httpClient,
|
||||||
private readonly SiteConfigRepository $configRepo,
|
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
// Nullable: env `%env(default::KAVENEGAR_API_KEY)%` resolves to null when unset
|
// API key comes only from env `%env(default::KAVENEGAR_API_KEY)%`; null when unset.
|
||||||
// (the real key normally comes from DB site config below, not env).
|
|
||||||
private readonly ?string $apiKey = null,
|
private readonly ?string $apiKey = null,
|
||||||
private readonly ?string $sender = null,
|
private readonly ?string $sender = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private function key(): string { return $this->configRepo->get('kavenegar_api_key') ?: ($this->apiKey ?? ''); }
|
private function key(): string { return $this->apiKey ?? ''; }
|
||||||
private function sender(): string { return $this->configRepo->get('kavenegar_sender') ?: ($this->sender ?? ''); }
|
private function sender(): string { return $this->sender ?? ''; }
|
||||||
|
|
||||||
public function getName(): string { return 'kavenegar'; }
|
public function getName(): string { return 'kavenegar'; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user