- 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.
11 KiB
نکات پیادهسازی — تسک ۰۱: راهاندازی پروژه
تفاوتهای اصلی 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)
# 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 خاص
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 — محدود، نه باز
# 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)%']
# .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)
// 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
# config/packages/nelmio_api_doc.yaml
when@prod:
nelmio_api_doc:
# در production کاملاً غیرفعال میشود
# route ها به /api/doc باید در routing فقط برای dev تعریف شوند
# 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
برای رویدادهای امنیتی حساس، یک جدول جداگانه وجود دارد:
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
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