Files
clinicpro/docs/tasks/task-01-project-setup/implementation_notes.md
T
hamed de1a78a235 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.
2026-06-09 22:00:34 +03:30

318 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نکات پیاده‌سازی — تسک ۰۱: راه‌اندازی پروژه
## تفاوت‌های اصلی 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
```