# نکات پیاده‌سازی — تسک ۰۱: راه‌اندازی پروژه

## تفاوت‌های اصلی 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
```
