Files
clinicpro/.claude/prompt/cors-regex-from-hosts-coolify-safe.md
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

199 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# رفع دائمی 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`) را به‌روز کن.