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

11 KiB
Raw Blame History

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

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