feat: Implement SMS sending functionality with KavehNegar and Rangineh providers

- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
@@ -0,0 +1,317 @@
# نکات پیاده‌سازی — تسک ۰۱: راه‌اندازی پروژه
## تفاوت‌های اصلی Drupal vs Symfony
### CSRF Token
در Drupal: endpoint مخصوص `GET /session/token` برای دریافت CSRF توکن وجود دارد.
در Symfony: چون API کاملاً stateless است و JWT استفاده می‌شود، CSRF Token
سنتی **نیاز نیست**. به جای آن، JWT در هر request ارسال می‌شود.
→ در TASK-02 endpoint ساختگی `/session/token` پیاده‌سازی می‌شود که یک مقدار تصادفی
برمی‌گرداند تا کلاینت موجود بدون تغییر کار کند.
### UUID
در Drupal: UUID داخلی Drupal مدیریت می‌شود.
در Symfony: از `symfony/uid` (built-in) استفاده کن — نه `ramsey/uuid`.
→ تمام ID های عمومی در API باید UUID باشند، نه auto-increment.
---
## پیکربندی Security (security.yaml)
```yaml
# config/packages/security.yaml
security:
password_hashers:
App\Auth\Entity\User:
algorithm: bcrypt
cost: 12
providers:
# تنها provider — username همیشه شماره موبایل است (برای همه نقش‌ها)
app_user_provider:
entity:
class: App\Auth\Entity\User
property: mobileNumber
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
health:
pattern: ^/health$
security: false
# Endpoints کاملاً عمومی (بدون هیچ بررسی)
public:
pattern: ^/(api/v1/user/send-code|api/v1/user/verify-code|api/v1/user/register|oauth/token|session/token)$
stateless: true
security: false
api:
pattern: ^/(api|oauth)/
stateless: true
# ۱) Password login برای doctor/clinic/secretary
custom_authenticators:
- App\Auth\Security\PasswordAuthenticator
# ۲) JWT middleware — Authorization: Bearer را می‌خواند
jwt: ~
access_control:
# Public endpoints
- { path: ^/health$, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/send-code, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/verify-code, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/register, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/user/login, roles: PUBLIC_ACCESS }
- { path: ^/oauth/token$, roles: PUBLIC_ACCESS }
- { path: ^/oauth/token/refresh$, roles: PUBLIC_ACCESS }
- { path: ^/session/token, roles: PUBLIC_ACCESS }
# Swagger — فقط در dev
- { path: ^/api/doc, roles: PUBLIC_ACCESS, env: dev }
# Admin-only
- { path: ^/api/v1/user/\d+$, methods: [DELETE], roles: ROLE_ADMIN }
# Authenticated
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/oauth/userinfo, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/oauth/logout, roles: IS_AUTHENTICATED_FULLY }
```
---
## ⚠ استاندارد JWT — Access Token در هدر، Refresh Token در Body
این مهم‌ترین نکته امنیتی در مدیریت توکن‌هاست. دو توکن دو جای کاملاً متفاوت دارند:
### Access Token → فقط در `Authorization` header
```
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...
```
**LexikJWTAuthenticationBundle** این هدر را به صورت خودکار در تمام endpoint های محافظت‌شده بررسی می‌کند.
در Controller کد اضافه‌ای لازم نیست.
**هرگز access_token را اینجا نفرست:**
```
❌ GET /api/endpoint?token=eyJ... ← URL — در لاگ‌های سرور ذخیره می‌شود
❌ POST body: {"token": "eyJ..."} ← Body — افشا در لاگ‌های request
❌ Cookie: access_token=eyJ... ← Cookie — باید HttpOnly باشد و CSRF لازم دارد
```
### Refresh Token → فقط در body برای یک endpoint خاص
```json
POST /oauth/token/refresh
Content-Type: application/json
{ "refresh_token": "a8f3b2c1d0e4..." }
```
Refresh Token هرگز در `Authorization` header نمی‌رود. این endpoint در `access_control` با `PUBLIC_ACCESS` است — تأیید هویت با خود Refresh Token انجام می‌شود.
### گردش کامل توکن‌ها
```
[ورود — یکبار]
POST /api/v1/user/login (password)
یا
POST /oauth/token (OTP)
← Response:
{
"access_token": "eyJ..." ← TTL=1h — در RAM/memory ذخیره کن
"refresh_token": "a8f3b2..." ← TTL=30d — در HttpOnly Cookie یا secure storage
}
[هر request محافظت‌شده]
Authorization: Bearer eyJ... ← فقط access_token
[وقتی access_token منقضی — 401 دریافت شد]
POST /oauth/token/refresh
{ "refresh_token": "a8f3b2..." }
← Response: access_token جدید + refresh_token جدید (Rotation)
[خروج]
POST /oauth/logout
Authorization: Bearer eyJ...
{ "refresh_token": "a8f3b2..." }
← هر دو توکن باطل می‌شوند
```
---
## پیکربندی CORS — محدود، نه باز
```yaml
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin:
- '%env(CORS_ALLOW_ORIGIN)%'
allow_methods: ['GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'Content-Disposition']
expose_headers: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset']
max_age: 3600
allow_credentials: false
paths:
'^/api/':
# ⚠️ هرگز '*' نگذار — فقط دامنه‌های مشخص
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
'^/oauth/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
'^/health':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
```
```dotenv
# .env.local (production)
CORS_ALLOW_ORIGIN=^https://(app\.clinicpro\.ir|admin\.clinicpro\.ir)$
# .env (development)
CORS_ALLOW_ORIGIN=^https?://(localhost|.*\.ddev\.site)(:\d+)?$
```
> **⚠ هشدار:** هرگز `allow_origin: ['*']` در production استفاده نکن.
> این اجازه می‌دهد هر وب‌سایت مخرب درخواست‌های authenticated ارسال کند.
---
## Security Headers (EventSubscriber)
```php
// src/Shared/EventSubscriber/SecurityHeadersSubscriber.php
class SecurityHeadersSubscriber implements EventSubscriberInterface
{
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) return;
$response = $event->getResponse();
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
if ($event->getRequest()->isSecure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
}
// برای API responses، Content-Security-Policy محدود
if (str_starts_with($event->getRequest()->getPathInfo(), '/api')) {
$response->headers->set('Content-Security-Policy', "default-src 'none'");
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::RESPONSE => 'onKernelResponse'];
}
}
```
---
## Swagger UI — فقط در محیط Dev
```yaml
# config/packages/nelmio_api_doc.yaml
when@prod:
nelmio_api_doc:
# در production کاملاً غیرفعال می‌شود
# route ها به /api/doc باید در routing فقط برای dev تعریف شوند
```
```yaml
# config/routes/nelmio_api_doc.yaml
when@dev:
app.swagger_ui:
path: /api/doc
methods: GET
defaults:
_controller: nelmio_api_doc.controller.swagger_ui
app.swagger_json:
path: /api/doc.json
methods: GET
defaults:
_controller: nelmio_api_doc.controller.swagger
```
> **⚠** با این روش، در production هیچ route ای برای `/api/doc` وجود ندارد → 404
---
## Audit Log — جدول security_logs
برای رویدادهای امنیتی حساس، یک جدول جداگانه وجود دارد:
```sql
CREATE TABLE security_logs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(50) NOT NULL, -- 'otp_failed', 'login_success', 'role_changed', 'payment_verified', ...
user_id INT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent VARCHAR(255) NULL,
details JSON NULL, -- اطلاعات اضافه (بدون data حساس!)
created_at INT NOT NULL
);
CREATE INDEX idx_sec_logs_event ON security_logs(event_type);
CREATE INDEX idx_sec_logs_user ON security_logs(user_id);
CREATE INDEX idx_sec_logs_created ON security_logs(created_at);
```
**رویدادهایی که باید log شوند:**
```
otp_sent — ارسال OTP (mobile ماسک‌شده: 0912***1713)
otp_failed — کد اشتباه
otp_expired — کد منقضی
login_success — ورود موفق
login_failed — ورود ناموفق
logout — خروج
token_refreshed — Refresh Token استفاده شد
role_changed — تغییر نقش کاربر
user_deleted — حذف کاربر
payment_initiated — شروع پرداخت
payment_verified — تأیید پرداخت
payment_failed — پرداخت ناموفق
file_uploaded — آپلود فایل
```
> **⚠ داده‌های حساس را log نکن:** شماره کامل موبایل، کد OTP، شماره کارت، JWT.
---
## نکات DDEV
```bash
ddev exec php bin/console ...
ddev composer ...
ddev describe # مشاهده آدرس‌ها و پورت‌ها
ddev ssh # ورود به container
```
URL پروژه: `https://clinic-pro.ddev.site`
---
## نکات امنیتی Production
```
✅ هرگز config/jwt/private.pem را در git commit نکن (.gitignore)
✅ JWT_PASSPHRASE را قوی انتخاب کن (حداقل 32 کاراکتر تصادفی)
✅ APP_SECRET را با openssl rand -hex 32 تولید کن
✅ .env.local برای production (نه .env)
✅ CORS_ALLOW_ORIGIN فقط دامنه‌های مشخص (نه *)
✅ Swagger UI فقط در dev فعال است
✅ APP_DEBUG=false در production
```