# رفع دائمی 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 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`) را به‌روز کن.