Files
clinicpro/.claude/prompt/cors-regex-from-hosts-coolify-safe.md
T
hamed 87f4d1695f Add CorsRegexEnvProcessor and corresponding tests
- Implemented CorsRegexEnvProcessor to build CORS origin regex from a comma-separated host list (ALLOWED_FRONTEND_HOSTS).
- Added tests for CorsRegexEnvProcessor to validate regex generation and matching behavior.
- Created JSON files for AST representation of the new classes and tests.
2026-07-07 14:58:41 +03:30

11 KiB
Raw Blame History

رفع دائمی 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:

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:

        $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

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 بماند.

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 ست نکند).

۴. تست

# ساخت 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) را به‌روز کن.