# رفع دائمی CORS در Coolify: ساخت regex از `ALLOWED_FRONTEND_HOSTS` (حذف وابستگی به `CORS_ALLOW_ORIGIN`)

## پروژه

`clinicpro` (Backend — CORS/nelmio)

## زمینه

فرانت‌اند عمومی (`nobat724_front`) روی دامنه‌های شهری (مثل `yasuj-nobat.ir`) هنگام
`POST /api/v1/user/send-code` این خطا را می‌گیرد:

```
Access to XMLHttpRequest at 'https://clinic-pro.ir/api/v1/user/send-code'
from origin 'https://yasuj-nobat.ir' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

تشخیص قطعی (تست شده): کد و کانفیگ nelmio **سالم** است — روی محیط لوکال ddev همان regex،
هدر `access-control-allow-origin` را درست برمی‌گرداند. اما روی پروداکشن، پاسخ preflight **هیچ**
هدر `access-control-allow-origin` ندارد. علت: **Coolify مقدار env متغیر `CORS_ALLOW_ORIGIN` را
خراب می‌کند**، چون آن مقدار یک regex است که با `$` تمام می‌شود و شامل `\` است؛ Coolify هنگام
interpolation، `$` و backslashها را دستکاری/escape می‌کند و در نهایت regex خالی/نادرست به
کانتینر می‌رسد → nelmio هیچ origin ای را match نمی‌کند → هدر ACAO ست نمی‌شود.

راه‌حل رسمی Coolify («Is Literal?») خودش باگ‌های گزارش‌شده دارد (مقدار را داخل `'...'` می‌پیچد یا
دوباره escape می‌کند). بنابراین به‌جای اتکا به یک env شکننده‌ی حاوی `$`/`\`, باید regex را
**سمت برنامه از روی یک env امن بسازیم**.

## مشکل / هدف

env امنِ `ALLOWED_FRONTEND_HOSTS` از قبل وجود دارد و **فقط شامل نام دامنه‌ها با کاما** است
(بدون `$`، بدون `\`, بدون کاراکتر خاص) → Coolify آن را خراب نمی‌کند. هدف:

1. یک **Env Var Processor** سفارشی (`cors_regex`) بساز که رشته‌ی کامادار `ALLOWED_FRONTEND_HOSTS`
   را بگیرد و همان regex نهایی CORS را در PHP بسازد.
2. `nelmio_cors.yaml` را طوری تغییر بده که از `%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%` استفاده کند.
3. متغیر `CORS_ALLOW_ORIGIN` را از فایل‌های نمونه‌ی env حذف/deprecate کن تا دیگر لازم نباشد در
   Coolify ست شود (منبع باگ حذف می‌شود).

نتیجه: تنها env مربوط به CORS یک لیست سادهٔ کامادارِ Coolify-safe است.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `config/packages/nelmio_cors.yaml` | فعلاً `allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']` با `origin_regex: true` |
| `config/services.yaml` | این‌جا `%env(ALLOWED_FRONTEND_HOSTS)%` به کنترلرها bind شده (خط ~۸۹) |
| `src/Shared/...` (جدید) | کلاس Env Var Processor سفارشی |
| `.env` | مقدار پیش‌فرض `CORS_ALLOW_ORIGIN` و `ALLOWED_FRONTEND_HOSTS` |
| `.env.coolify.example` / `.env.liara.example` | نمونه‌ی env برای دیپلوی — باید به‌روز شوند |
| `docker/gen-cors-env.php` | تولیدکننده‌ی خروجی CORS از `docker/frontend-domains.json` (بررسی هم‌خوانی) |

## وضعیت فعلی

`config/packages/nelmio_cors.yaml`:

```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)%'] }
```

`config/services.yaml` (خط ~۸۹) — الگوی bind فعلی env:

```yaml
        $allowedFrontendHosts: '%env(ALLOWED_FRONTEND_HOSTS)%'
```

`.env` (مقدار پیش‌فرض فعلی که در پروداکشن خراب می‌شود):

```
CORS_ALLOW_ORIGIN='^https?://([a-z0-9-]+\.)*(clinic-pro\.ddev\.site|localhost|127\.0\.0\.1|[a-z0-9-]+-nobat\.ir|nobat724\.com)(:[0-9]+)?$'
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,...,zanjan-nobat.ir
```

## وظایف

### ۱. ساخت Env Var Processor سفارشی `cors_regex`

یک کلاس بساز (مثلاً `src/Shared/DependencyInjection/CorsRegexEnvProcessor.php`) که
`Symfony\Component\DependencyInjection\EnvVarProcessorInterface` را پیاده کند. ورودی یک نام env
(`ALLOWED_FRONTEND_HOSTS`) است؛ خروجی یک regex معتبر PCRE برای nelmio.

```php
<?php

namespace App\Shared\DependencyInjection;

use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;

final class CorsRegexEnvProcessor implements EnvVarProcessorInterface
{
    public function getEnv(string $prefix, string $name, \Closure $getEnv): string
    {
        $raw = (string) $getEnv($name);                 // "ahvaz-nobat.ir,arak-nobat.ir,..."
        $hosts = array_filter(array_map('trim', explode(',', $raw)));

        if ($hosts === []) {
            // fail-safe: هیچ origin ای مجاز نیست (بهتر از regex خالیِ نامعتبر)
            return '^https://$a';
        }

        $alts = implode('|', array_map(
            static fn (string $h) => preg_quote($h, '#'),
            $hosts
        ));

        // با یا بدون زیر‌دامنه، فقط https، انکورشده. بدون نیاز به هیچ env حاوی $.
        return '^https://([a-z0-9-]+\.)*(' . $alts . ')$';
    }

    public static function getProvidedTypes(): array
    {
        return ['cors_regex' => 'string'];
    }
}
```

> اگر پورت برای محیط لوکال لازم است (`localhost:3000`)، الگو را طوری بساز که پورت اختیاری را هم
> بپذیرد؛ اما چون `ALLOWED_FRONTEND_HOSTS` فقط دامنه‌های پروداکشن است، برای dev می‌توان `localhost`
> را هم در همان لیست env محیط dev گذاشت یا در processor به‌صورت شرطی افزود. تصمیم را در پیاده‌سازی
> صریح بگیر و ساده نگه‌دار.

اطمینان حاصل کن کلاس به‌عنوان env processor شناخته می‌شود: چون `EnvVarProcessorInterface` را
پیاده می‌کند و autoconfigure روشن است، Symfony آن را خودکار با تگ
`container.env_var_processor` ثبت می‌کند. اگر autowire/autoconfigure برای این namespace فعال
نیست، در `config/services.yaml` صریح ثبتش کن.

### ۲. تغییر `nelmio_cors.yaml` برای استفاده از processor

همه‌ی `%env(CORS_ALLOW_ORIGIN)%` را با `%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%` جایگزین کن.
`origin_regex: true` بماند.

```yaml
nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
        # ... بقیه بدون تغییر ...
    paths:
        '^/api/':    { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
        '^/oauth/':  { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
        '^/health':  { allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%'] }
```

### ۳. به‌روزرسانی فایل‌های env

- `.env`: می‌توانی `CORS_ALLOW_ORIGIN` را نگه داری اما دیگر مصرف نمی‌شود؛ بهتر است حذف/کامنت شود
  تا گمراه‌کننده نباشد. `ALLOWED_FRONTEND_HOSTS` باید شامل دامنه‌های dev هم باشد اگر لازم است
  (`clinic-pro.ddev.site`,`localhost` و ...) — تصمیم بگیر و مستند کن.
- `.env.coolify.example` و `.env.liara.example`: بخش CORS را بازنویسی کن — توضیح بده که فقط
  `ALLOWED_FRONTEND_HOSTS` (لیست کامادار، بدون کاراکتر خاص، Coolify-safe) لازم است و
  `CORS_ALLOW_ORIGIN` دیگر استفاده نمی‌شود (تا کاربر آن را در Coolify ست نکند).

### ۴. تست

```bash
# ساخت regex درست است؟ (کانفیگ نهایی nelmio را چاپ کن)
ddev exec php bin/console cache:clear
ddev exec php bin/console debug:config nelmio_cors 2>&1 | grep -i allow_origin

# preflight لوکال باید ACAO بدهد:
curl -sk -i -X OPTIONS "https://clinic-pro.ddev.site/api/v1/user/send-code" \
  -H "Origin: https://yasuj-nobat.ir" -H "Access-Control-Request-Method: POST" \
  | grep -i "access-control-allow-origin"

# origin نامعتبر نباید ACAO بگیرد:
curl -sk -i -X OPTIONS "https://clinic-pro.ddev.site/api/v1/user/send-code" \
  -H "Origin: https://evil.com" -H "Access-Control-Request-Method: POST" \
  | grep -i "access-control-allow-origin"   # باید خالی باشد
```

هر دو حالت باید درست کار کنند (origin مجاز ACAO بگیرد، نامجاز نگیرد).

## نکات مهم

- **هیچ env حاوی `$` یا `\` نباید برای CORS لازم باشد** — کل هدف همین است؛ `ALLOWED_FRONTEND_HOSTS`
  فقط دامنه‌های کامادار است و Coolify آن را دست‌نخورده نگه می‌دارد.
- `preg_quote($h, '#')` برای escape نقطه‌ها ضروری است (چون nelmio با delimiter `#` regex را
  کامپایل می‌کند — الگو را با همان delimiter سازگار نگه‌دار؛ اگر nelmio delimiter دیگری می‌گذارد،
  با تست `debug:config` و preflight تأیید کن).
- fail-safe: اگر لیست خالی بود، یک regex بساز که **هیچ‌چیز** را match نکند (نه یک regex خالی که
  ممکن است به‌طور ناخواسته همه را رد یا قبول کند).
- deploy: بعد از merge، در Coolify فقط `ALLOWED_FRONTEND_HOSTS` را نگه دار و `CORS_ALLOW_ORIGIN`
  را حذف کن؛ سپس **redeploy**. با `printenv ALLOWED_FRONTEND_HOSTS` داخل کانتینر صحت مقدار را چک کن.
- این تغییر فقط کانفیگ/DI است؛ Entity/migration ندارد. مستندات API عوض نمی‌شود (رفتار endpointها
  ثابت است)، اما اگر جایی CORS مستند شده، اشاره به منبع جدید (`ALLOWED_FRONTEND_HOSTS`) را به‌روز کن.
