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.
This commit is contained in:
hamed
2026-07-07 14:58:41 +03:30
parent 4ee1524f31
commit 87f4d1695f
18 changed files with 2536 additions and 1343 deletions
@@ -0,0 +1,198 @@
# رفع دائمی 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`) را به‌روز کن.
+9 -10
View File
@@ -25,16 +25,16 @@ MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages"
# This is the API host, NOT a frontend domain.
APP_BASE_URL=https://api.nobat724.com
# ── Frontend domains (MANY) ──
# Both values below are GENERATED from docker/frontend-domains.json.
# To add/remove a city domain: edit that file, then run:
# ddev exec php docker/gen-cors-env.php (or: php docker/gen-cors-env.php on the server)
# and paste the new output here / into Coolify.
# ── Frontend domains (MANY) — CORS ──
# فقط همین یک متغیرِ سادهٔ کامادار لازم است؛ رجکسِ CORS در PHP از روی همین ساخته می‌شود
# (CorsRegexEnvProcessor → nelmio). چون این مقدار هیچ کاراکتر خاصی (`$`,`\`) ندارد، Coolify
# آن را خراب نمی‌کند — برخلاف CORS_ALLOW_ORIGIN که حذف شده است.
# افزودن/حذف دامنهٔ شهر: docker/frontend-domains.json را ویرایش کن، سپس
# ddev exec php docker/gen-cors-env.php (یا روی سرور: php docker/gen-cors-env.php)
# و خروجی ALLOWED_FRONTEND_HOSTS را این‌جا/در Coolify جایگزین کن.
# ⚠️ دیگر CORS_ALLOW_ORIGIN را ست نکن (استفاده نمی‌شود).
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
CORS_ALLOW_ORIGIN='^https://(ahvaz\-nobat\.ir|arak\-nobat\.ir|ardabil\-nobat\.ir|bandar\-nobat\.ir|behbahan\-nobat\.ir|birjand\-nobat\.ir|bojnord\-nobat\.ir|bushehr\-nobat\.ir|dehdasht\-nobat\.ir|esf\-nobat\.ir|golestan\-nobat\.ir|hamadan\-nobat\.ir|ilam\-nobat\.ir|karaj\-nobat\.ir|kerman\-nobat\.ir|kermanshah\-nobat\.ir|lorestan\-nobat\.ir|mashhad\-nobat\.ir|nobat724\.com|qazvin\-nobat\.ir|qom\-nobat\.ir|rasht\-nobat\.ir|sanandaj\-nobat\.ir|sari\-nobat\.ir|semnan\-nobat\.ir|shiraz\-nobat\.ir|shkord\-nobat\.ir|tabriz\-nobat\.ir|tehran\-nobat\.ir|urmia\-nobat\.ir|yasuj\-nobat\.ir|yazd\-nobat\.ir|zahedan\-nobat\.ir|zanjan\-nobat\.ir)$'
# ── Secrets (REQUIRED — set before the first deploy) ──
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
JWT_PASSPHRASE= # openssl rand -hex 32 (must exist before first start: JWT keypair is generated with it)
@@ -75,8 +75,7 @@ JWT_PASSPHRASE=...
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
ALLOWED_FRONTEND_HOSTS=... # همان خروجی gen-cors-env.php
CORS_ALLOW_ORIGIN=... # همان
ALLOWED_FRONTEND_HOSTS=... # همان خروجی gen-cors-env.php (رجکس CORS از همین ساخته می‌شود)
API_IR_BASE_URL=https://s.api.ir
API_IR_TOKEN=...
REFRESH_TOKEN_TTL / OTP_TTL / MAX_FILE_SIZE_BYTES / UPLOAD_DIR
+2 -2
View File
@@ -22,8 +22,8 @@ JWT_PASSPHRASE=CHANGE_ME_STRONG_PASSPHRASE
###< lexik/jwt-authentication-bundle ###
###> nelmio/cors-bundle ###
# regex است (origin_regex: true). همهٔ دامنه‌های عمومی چند-شهری «<city>-nobat.ir» و پنل «clinic-pro.ir» را پوشش می‌دهد.
CORS_ALLOW_ORIGIN='^https://([a-z0-9-]+\.)*([a-z0-9-]+-nobat\.ir|clinic-pro\.ir)$'
# رجکسِ CORS در PHP از روی ALLOWED_FRONTEND_HOSTS ساخته می‌شود (CorsRegexEnvProcessor).
# CORS_ALLOW_ORIGIN دیگر استفاده نمی‌شود — ست نکن.
###< nelmio/cors-bundle ###
###> symfony/messenger ###
+5 -6
View File
@@ -37,12 +37,11 @@ DEFAULT_URI=https://<your-liara-domain>
# ── Reverse proxy (Liara router) — trust X-Forwarded-* from the private ranges ──
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
# ── Frontend domains (MANY) — GENERATED from docker/frontend-domains.json ──
# To add/remove a city domain: edit that file, then run:
# ddev exec php docker/gen-cors-env.php (or: php docker/gen-cors-env.php on the server)
# and paste the new output into both vars below.
CORS_ALLOW_ORIGIN='^https://(ahvaz\-nobat\.ir|arak\-nobat\.ir|ardabil\-nobat\.ir|bandar\-nobat\.ir|behbahan\-nobat\.ir|birjand\-nobat\.ir|bojnord\-nobat\.ir|bushehr\-nobat\.ir|dehdasht\-nobat\.ir|esf\-nobat\.ir|golestan\-nobat\.ir|hamadan\-nobat\.ir|ilam\-nobat\.ir|karaj\-nobat\.ir|kerman\-nobat\.ir|kermanshah\-nobat\.ir|lorestan\-nobat\.ir|mashhad\-nobat\.ir|nobat724\.com|qazvin\-nobat\.ir|qom\-nobat\.ir|rasht\-nobat\.ir|sanandaj\-nobat\.ir|sari\-nobat\.ir|semnan\-nobat\.ir|shiraz\-nobat\.ir|shkord\-nobat\.ir|tabriz\-nobat\.ir|tehran\-nobat\.ir|urmia\-nobat\.ir|yasuj\-nobat\.ir|yazd\-nobat\.ir|zahedan\-nobat\.ir|zanjan\-nobat\.ir)$'
# ── Frontend domains (MANY) — CORS ──
# فقط همین متغیرِ سادهٔ کامادار لازم است؛ رجکسِ CORS در PHP از روی همین ساخته می‌شود
# (CorsRegexEnvProcessor → nelmio). CORS_ALLOW_ORIGIN حذف شده — دیگر ست نکن.
# افزودن/حذف دامنه: docker/frontend-domains.json را ویرایش کن، سپس گرفتن خروجی جدید:
# ddev exec php docker/gen-cors-env.php (یا روی سرور: php docker/gen-cors-env.php)
ALLOWED_FRONTEND_HOSTS=ahvaz-nobat.ir,arak-nobat.ir,ardabil-nobat.ir,bandar-nobat.ir,behbahan-nobat.ir,birjand-nobat.ir,bojnord-nobat.ir,bushehr-nobat.ir,dehdasht-nobat.ir,esf-nobat.ir,golestan-nobat.ir,hamadan-nobat.ir,ilam-nobat.ir,karaj-nobat.ir,kerman-nobat.ir,kermanshah-nobat.ir,lorestan-nobat.ir,mashhad-nobat.ir,nobat724.com,qazvin-nobat.ir,qom-nobat.ir,rasht-nobat.ir,sanandaj-nobat.ir,sari-nobat.ir,semnan-nobat.ir,shiraz-nobat.ir,shkord-nobat.ir,tabriz-nobat.ir,tehran-nobat.ir,urmia-nobat.ir,yasuj-nobat.ir,yazd-nobat.ir,zahedan-nobat.ir,zanjan-nobat.ir
# ── api.ir identity inquiry (Shahkar + IbanMatch) ──
+4 -4
View File
@@ -1,7 +1,7 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
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']
@@ -9,8 +9,8 @@ nelmio_cors:
allow_credentials: false
paths:
'^/api/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
'^/oauth/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
'^/health':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_origin: ['%env(cors_regex:ALLOWED_FRONTEND_HOSTS)%']
+3 -3
View File
@@ -25,7 +25,6 @@ x-app-env: &app-env
JWT_SECRET_KEY: "%kernel.project_dir%/config/jwt/private.pem"
JWT_PUBLIC_KEY: "%kernel.project_dir%/config/jwt/public.pem"
JWT_PASSPHRASE: ${JWT_PASSPHRASE}
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN}
DEFAULT_URI: ${APP_BASE_URL}
APP_BASE_URL: ${APP_BASE_URL}
ALLOWED_FRONTEND_HOSTS: ${ALLOWED_FRONTEND_HOSTS}
@@ -48,8 +47,9 @@ services:
# In Coolify, assign ALL serving domains to THIS service (port 80) — the backend
# API host plus every frontend domain you want Traefik to route + issue TLS for.
# Coolify supports a comma-separated domain list on the service.
# CORS/payload allow-listing for those frontends is handled separately via
# CORS_ALLOW_ORIGIN / ALLOWED_FRONTEND_HOSTS (see docker/frontend-domains.json).
# CORS/payload allow-listing for those frontends is driven by ALLOWED_FRONTEND_HOSTS
# (the CORS regex is built from it in PHP via CorsRegexEnvProcessor; see
# docker/frontend-domains.json). CORS_ALLOW_ORIGIN is no longer used.
app:
# Shared image tag — app builds it once; the two workers reuse the SAME image
# (image: below, no build:). Without this Coolify builds the Dockerfile 3×,
+7 -9
View File
@@ -6,9 +6,11 @@
* Usage:
* php docker/gen-cors-env.php
*
* Output: CORS_ALLOW_ORIGIN (single regex, explicit alternation) and
* ALLOWED_FRONTEND_HOSTS (comma-separated host list).
* Paste both into the Coolify Environment Variables tab.
* Output: ALLOWED_FRONTEND_HOSTS (comma-separated host list).
* Paste it into the Coolify Environment Variables tab. The CORS origin regex is
* built in PHP from this list (App\Shared\DependencyInjection\CorsRegexEnvProcessor),
* so CORS_ALLOW_ORIGIN is no longer needed — a `$`-containing regex env gets mangled
* by Coolify's interpolation, which is exactly the bug this avoids.
*/
$jsonFile = __DIR__ . '/frontend-domains.json';
@@ -50,13 +52,9 @@ if (empty($domains)) {
exit(1);
}
// CORS: explicit alternation, anchored, dots escaped. https only.
$alternation = implode('|', array_map(static fn (string $d): string => preg_quote($d, '/'), $domains));
$cors = "^https://($alternation)$";
// Frontend hosts: bare hostnames, comma-separated (matched via in_array in PaymentController).
// Frontend hosts: bare hostnames, comma-separated. Drives both the CORS regex
// (CorsRegexEnvProcessor) and PaymentController host validation.
$hosts = implode(',', $domains);
echo "# ---- paste into Coolify env (" . count($domains) . " domains) ----\n\n";
echo "CORS_ALLOW_ORIGIN='" . $cors . "'\n\n";
echo "ALLOWED_FRONTEND_HOSTS=" . $hosts . "\n";
+29 -1
View File
@@ -695,5 +695,33 @@
"693": "Community 693",
"694": "Community 694",
"695": "Community 695",
"696": "Community 696"
"696": "Community 696",
"697": "Community 697",
"698": "Community 698",
"699": "Community 699",
"700": "Community 700",
"701": "Community 701",
"702": "Community 702",
"703": "Community 703",
"704": "Community 704",
"705": "Community 705",
"706": "Community 706",
"707": "Community 707",
"708": "Community 708",
"709": "Community 709",
"710": "Community 710",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"716": "Community 716",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"720": "Community 720",
"721": "Community 721",
"722": "Community 722",
"723": "Community 723",
"724": "Community 724"
}
+224 -96
View File
@@ -1,16 +1,16 @@
# Graph Report - clinicpro (2026-07-07)
## Corpus Check
- 708 files · ~520,035 words
- 711 files · ~521,407 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 8966 nodes · 12391 edges · 697 communities (560 shown, 137 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 275 edges (avg confidence: 0.8)
- 8995 nodes · 12432 edges · 725 communities (585 shown, 140 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 277 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `57d08124`
- Built from commit: `4ee1524f`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -697,6 +697,34 @@
- [[_COMMUNITY_Community 694|Community 694]]
- [[_COMMUNITY_Community 695|Community 695]]
- [[_COMMUNITY_Community 696|Community 696]]
- [[_COMMUNITY_Community 697|Community 697]]
- [[_COMMUNITY_Community 698|Community 698]]
- [[_COMMUNITY_Community 699|Community 699]]
- [[_COMMUNITY_Community 700|Community 700]]
- [[_COMMUNITY_Community 701|Community 701]]
- [[_COMMUNITY_Community 702|Community 702]]
- [[_COMMUNITY_Community 703|Community 703]]
- [[_COMMUNITY_Community 704|Community 704]]
- [[_COMMUNITY_Community 705|Community 705]]
- [[_COMMUNITY_Community 706|Community 706]]
- [[_COMMUNITY_Community 707|Community 707]]
- [[_COMMUNITY_Community 708|Community 708]]
- [[_COMMUNITY_Community 709|Community 709]]
- [[_COMMUNITY_Community 710|Community 710]]
- [[_COMMUNITY_Community 711|Community 711]]
- [[_COMMUNITY_Community 712|Community 712]]
- [[_COMMUNITY_Community 713|Community 713]]
- [[_COMMUNITY_Community 714|Community 714]]
- [[_COMMUNITY_Community 715|Community 715]]
- [[_COMMUNITY_Community 716|Community 716]]
- [[_COMMUNITY_Community 717|Community 717]]
- [[_COMMUNITY_Community 718|Community 718]]
- [[_COMMUNITY_Community 719|Community 719]]
- [[_COMMUNITY_Community 720|Community 720]]
- [[_COMMUNITY_Community 721|Community 721]]
- [[_COMMUNITY_Community 722|Community 722]]
- [[_COMMUNITY_Community 723|Community 723]]
- [[_COMMUNITY_Community 724|Community 724]]
## God Nodes (most connected - your core abstractions)
1. `BaseController` - 76 edges
@@ -715,8 +743,8 @@
assets/admin/components/ServiceTariffModal.tsx → assets/admin/lib/utils.ts
- `Pagination()` --calls--> `formatNumber()` [EXTRACTED]
assets/admin/components/ui/Pagination.tsx → assets/admin/lib/utils.ts
- `RepresentationSettlementPage()` --calls--> `formatRial()` [EXTRACTED]
assets/admin/pages/RepresentationSettlementPage.tsx → assets/admin/lib/utils.ts
- `PersianDatePicker()` --calls--> `formatDate()` [EXTRACTED]
assets/admin/components/ui/PersianDatePicker.tsx → assets/admin/lib/utils.ts
- `SettlementsPage()` --calls--> `formatRial()` [EXTRACTED]
assets/admin/pages/SettlementsPage.tsx → assets/admin/lib/utils.ts
- `NewAppointmentModal()` --calls--> `useAuthStore` [EXTRACTED]
@@ -725,11 +753,11 @@
## Import Cycles
- None detected.
## Communities (697 total, 137 thin omitted)
## Communities (725 total, 140 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (38): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, DEGREE_OPTIONS, DoctorFormPage() (+30 more)
Nodes (37): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, DEGREE_OPTIONS, DoctorFormPage() (+29 more)
### Community 1 - "Community 1"
Cohesion: 0.03
@@ -744,16 +772,16 @@ Cohesion: 0.10
Nodes (20): `RepresentationActionController` — welcome به‌صورت inline (بدون تمپلت), `SmsMessageTemplate::DEFAULTS` (فاقد نام تمپلت و نگاشت token), `SmsService::sendNow` — انتخاب بین lookup و متن‌آزاد, الگوی فعلی همه‌ی call-siteها (به‌جز OTP) — متن‌آزاد، بدون `templateCode`, تبدیل همه‌ی پیامک‌های سیستمی به VerifyLookup کاوه‌نگار (تمپلت نام‌دار), تنها جای درست (OTP) — که باید الگوی بقیه شود, زمینه, فایل‌های مرتبط (+12 more)
### Community 4 - "Community 4"
Cohesion: 0.05
Nodes (11): DoctorServiceController, DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule (+3 more)
Cohesion: 0.06
Nodes (8): DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule, ManagerRegistry
### Community 5 - "Community 5"
Cohesion: 0.07
Nodes (3): UserProfile, self, User
### Community 6 - "Community 6"
Cohesion: 0.09
Nodes (15): SettlementController, SettlementRepository, WalletTransactionRepository, CommissionService, Settlement, JsonResponse, Request, User (+7 more)
Cohesion: 0.12
Nodes (12): SettlementController, SettlementRepository, WalletTransactionRepository, Settlement, JsonResponse, Request, User, ManagerRegistry (+4 more)
### Community 7 - "Community 7"
Cohesion: 0.07
@@ -777,11 +805,11 @@ Nodes (4): UserActiveContextRepository, ManagerRegistry, User, UserActiveContext
### Community 12 - "Community 12"
Cohesion: 0.05
Nodes (39): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+31 more)
Nodes (40): Clinic Doctor Invitation API, Errors, Errors, Errors, Errors, Errors, Errors, Errors (+32 more)
### Community 13 - "Community 13"
Cohesion: 0.07
Nodes (25): get, api, ApiError, getToken(), refreshOnce(), request(), { refreshMock, logoutMock }, replaceMock (+17 more)
Cohesion: 0.08
Nodes (22): get, PaymentConfig, PaymentGatewayInfo, api, ApiError, getToken(), refreshOnce(), request() (+14 more)
### Community 14 - "Community 14"
Cohesion: 0.10
@@ -792,8 +820,8 @@ Cohesion: 0.25
Nodes (7): PaymentController, Appointment, JsonResponse, Payment, Request, Response, User
### Community 16 - "Community 16"
Cohesion: 0.09
Nodes (6): PatientSession, Appointment, Collection, PatientRecord, self, SessionService
Cohesion: 0.07
Nodes (7): PatientSession, SmsWallet, Appointment, Collection, PatientRecord, self, SessionService
### Community 17 - "Community 17"
Cohesion: 0.05
@@ -804,20 +832,20 @@ Cohesion: 0.05
Nodes (38): API, API, API, API, API, Route, Route, Route (+30 more)
### Community 19 - "Community 19"
Cohesion: 0.03
Nodes (81): PaginatedResponse, CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm (+73 more)
Cohesion: 0.04
Nodes (56): PaginatedResponse, formatDate(), STATUS_FILTERS, AddForm, addSchema, ClinicsPage(), HUES_LIST, FILTERS (+48 more)
### Community 20 - "Community 20"
Cohesion: 0.13
Cohesion: 0.14
Nodes (3): AdminApiController, JsonResponse, Request
### Community 21 - "Community 21"
Cohesion: 0.04
Nodes (59): PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), ApiResponse, formatNumber(), formatRial(), ClinicDetailPage(), AdminCharts (+51 more)
Cohesion: 0.05
Nodes (33): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+25 more)
### Community 22 - "Community 22"
Cohesion: 0.12
Nodes (10): RatingController, Like, CommentListNPlusOneTest, LikeRepository, JsonResponse, Request, User, Comment (+2 more)
Cohesion: 0.15
Nodes (9): RatingController, Like, LikeRepository, JsonResponse, Request, User, Comment, ManagerRegistry (+1 more)
### Community 23 - "Community 23"
Cohesion: 0.05
@@ -847,10 +875,6 @@ Nodes (4): Appointment, Doctor, self, User
Cohesion: 0.06
Nodes (35): Bulk import / export, DELETE `/api/v1/admin/city/{id}`, DELETE `/api/v1/admin/province/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/cities` (+27 more)
### Community 30 - "Community 30"
Cohesion: 0.06
Nodes (19): BillingCalculatorTest, BillingCalculator, GatewayFactory, MellatGateway, Money, MellatGatewayTest, SepGateway, BillingCalculator (+11 more)
### Community 31 - "Community 31"
Cohesion: 0.06
Nodes (33): `AppointmentStatusDropdown.tsx`, Doctor Tabs در داشبورد کلینیک, Pagination, `PersianCalendar.tsx`, Stats Bar, Status Badge (inline قابل کلیک), Status Dropdown (کلیک روی badge), Toolbar (+25 more)
@@ -937,7 +961,7 @@ Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, با
### Community 53 - "Community 53"
Cohesion: 0.08
Nodes (15): AppLogRepository, PaymentLog, ClaimItemRepository, PaymentLogRepository, PreRegistrationRepository, SiteConfigRepository, SmsTemplateRepository, ServiceEntityRepository (+7 more)
Nodes (15): AppLogRepository, ClaimItemRepository, ClinicStaffRepository, PreRegistrationRepository, SessionServiceRepository, SiteConfigRepository, ServiceEntityRepository, ManagerRegistry (+7 more)
### Community 54 - "Community 54"
Cohesion: 0.10
@@ -976,8 +1000,8 @@ Cohesion: 0.08
Nodes (25): Bulk import / export, DELETE `/api/v1/admin/specialty/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/specialties`, GET `/api/v1/specialties` (+17 more)
### Community 63 - "Community 63"
Cohesion: 0.22
Nodes (5): AuthController, RateLimiterFactory, JsonResponse, Request, User
Cohesion: 0.05
Nodes (31): AbstractAuthenticator, AuthenticationException, ErrorCodes, AuthController, ClinicServiceController, PatientController, StaffController, AppException (+23 more)
### Community 64 - "Community 64"
Cohesion: 0.29
@@ -987,10 +1011,6 @@ Nodes (4): SmsWalletController, JsonResponse, Request, User
Cohesion: 0.08
Nodes (24): 2. کاربر (User), 4. 🔵 `POST` verify code, 5. 🔵 `POST` send code, 6. 🔵 `POST` register, 7. 🔴 `DELETE` delete user, 8. 🟡 `PATCH` patch, 9. 🟢 `GET` list secretary, Request Body (+16 more)
### Community 66 - "Community 66"
Cohesion: 0.06
Nodes (6): ClinicStaff, SmsWallet, AppLog, LogPruneService, AppointmentExpiryService, self
### Community 67 - "Community 67"
Cohesion: 0.11
Nodes (4): Invoice, Collection, InvoiceItem, self
@@ -1008,8 +1028,8 @@ Cohesion: 0.15
Nodes (12): رفع بهم‌ریختگی کامل پنل ادمین روی iPhone 8 (Safari/Chrome iOS), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more)
### Community 71 - "Community 71"
Cohesion: 0.09
Nodes (21): AddForm, addSchema, ClinicsPage(), HUES_LIST, EMPTY, PreRegistration, STATUS_META, STATUS_TABS (+13 more)
Cohesion: 0.20
Nodes (8): AdminUser, ChangeRoleModal(), getPrimaryRole(), HUES_LIST, ROLE_META, ROLE_TABS, RoleBadge(), UserStats
### Community 72 - "Community 72"
Cohesion: 0.09
@@ -1043,6 +1063,10 @@ Nodes (3): SubscriptionPlan, Collection, self
Cohesion: 0.09
Nodes (23): dependencies, @ckeditor/ckeditor5-build-classic, @ckeditor/ckeditor5-react, @fontsource/vazirmatn, @heroicons/react, @hookform/resolvers, jalaali-js, leaflet (+15 more)
### Community 82 - "Community 82"
Cohesion: 0.07
Nodes (8): CategoryImportTest, Connection, RepositoryClassMappingTest, KernelTestCase, DbLogger, CategoryImporter, DbLoggerTest, Stringable
### Community 83 - "Community 83"
Cohesion: 0.09
Nodes (21): Backend, CSS / UI, Frontend, روند اجرای هر قابلیت, قبل از شروع — تحلیل پرامپت و ساخت Todo, قوانین اجرا (اجباری — هیچ استثنایی ندارد), قوانین خاص این پروژه, مثال اجرا (+13 more)
@@ -1057,7 +1081,7 @@ Nodes (30): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react
### Community 86 - "Community 86"
Cohesion: 0.06
Nodes (15): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemStaffOwnershipTest, UniqueConstraintsTest, EntityManagerInterface, KernelBrowser (+7 more)
Nodes (15): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser, CommentPaginationTest (+7 more)
### Community 87 - "Community 87"
Cohesion: 0.10
@@ -1128,8 +1152,8 @@ Cohesion: 0.11
Nodes (18): `AdminApiController::paymentDetail` — الان فقط GET, `MellatGateway.php` — الگوی موجود REST/SOAP (پس از کار sandbox), `PaymentGatewayInterface.php`, `PaymentManager.php` — الگوی log و transaction, برگشت/استرداد وجه ملت از پنل ادمین (bpReversalRequest / bpRefundRequest), زمینه, فایل‌های مرتبط, نکات مهم (+10 more)
### Community 104 - "Community 104"
Cohesion: 0.18
Nodes (6): TagController, TagRepository, JsonResponse, Request, ManagerRegistry, Tag
Cohesion: 0.35
Nodes (3): TagController, JsonResponse, Request
### Community 105 - "Community 105"
Cohesion: 0.11
@@ -1144,8 +1168,8 @@ Cohesion: 0.32
Nodes (6): AppointmentController, Appointment, Doctor, JsonResponse, Request, User
### Community 108 - "Community 108"
Cohesion: 0.13
Nodes (10): BaseController, CategoryController, CategoryImportController, SmsMessageController, JsonResponse, JsonResponse, Request, JsonResponse (+2 more)
Cohesion: 0.18
Nodes (7): BaseController, CategoryController, CategoryImportController, JsonResponse, JsonResponse, Request, JsonResponse
### Community 109 - "Community 109"
Cohesion: 0.29
@@ -1376,8 +1400,8 @@ Cohesion: 0.10
Nodes (20): api.ir (استعلام هویت — Shahkar / IbanMatch), اتصال به دیتابیس‌های مستقل (الزامی), اسرار (الزامی — قبل از اولین دیپلوی), امنیت و منابع, بررسی سلامت, دامنه‌ها و CORS, دیپلوی‌های بعدی, راهنمای دیپلوی ClinicPro (Coolify + Docker Compose) (+12 more)
### Community 169 - "Community 169"
Cohesion: 0.31
Nodes (6): ErrorCodes, ClinicServiceController, JsonResponse, Request, ServiceSection, User
Cohesion: 0.04
Nodes (46): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm, provinceSchema (+38 more)
### Community 170 - "Community 170"
Cohesion: 0.13
@@ -1400,16 +1424,16 @@ Cohesion: 0.14
Nodes (14): Doctor Management, Errors, Errors, GET `/api/v1/admin/doctors`, GET `/api/v1/admin/doctors/stats`, POST `/api/v1/admin/clinic`, POST `/api/v1/admin/doctors`, POST `/api/v1/admin/doctors/{uuid}/status` (+6 more)
### Community 175 - "Community 175"
Cohesion: 0.35
Nodes (5): PatientController, JsonResponse, PatientSession, Request, User
Cohesion: 0.13
Nodes (12): AdminUserDetail, AVATAR_COLORS, EditForm, editSchema, GENDER_LABELS, getPrimaryRole(), MARITAL_LABELS, MEDICAL_SECTIONS (+4 more)
### Community 176 - "Community 176"
Cohesion: 0.25
Nodes (4): PaymentManager, Payment, PaymentInitResult, PaymentRefundResult
### Community 177 - "Community 177"
Cohesion: 0.36
Nodes (5): StaffController, ClinicStaff, JsonResponse, Request, User
Cohesion: 0.15
Nodes (12): رفع دائمی CORS در Coolify: ساخت regex از `ALLOWED_FRONTEND_HOSTS` (حذف وابستگی به `CORS_ALLOW_ORIGIN`), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more)
### Community 178 - "Community 178"
Cohesion: 0.14
@@ -1508,8 +1532,8 @@ Cohesion: 0.15
Nodes (12): Clinic Services API, DELETE /api/v1/service-item/{uuid}, DELETE /api/v1/service-section/{uuid}, GET /api/v1/service-items/{sectionUuid}, GET /api/v1/service-items/{uuid}/tariffs, GET /api/v1/service-sections, PATCH /api/v1/service-item/{uuid}, PATCH /api/v1/service-section/{uuid} (+4 more)
### Community 204 - "Community 204"
Cohesion: 0.21
Nodes (6): AuthenticationException, ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
Cohesion: 0.23
Nodes (5): ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
### Community 205 - "Community 205"
Cohesion: 0.10
@@ -1520,8 +1544,8 @@ Cohesion: 0.08
Nodes (14): MellatGateway, MockGateway, SepGateway, SoapClient, PaymentGatewayInterface, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult (+6 more)
### Community 207 - "Community 207"
Cohesion: 0.07
Nodes (30): ۲. مدل داده و موجودیت‌ها, ۲.۱ کاربر (User), ۲.۱۰ پرداخت (Payment Types), ۲.۱۰.۱ پرداخت نوبت, ۲.۱۰.۲ پرداخت اشتراک, ۲.۱۱ پروفایل بیمار (Profile), ۲.۱۲ وبلاگ (Blog), ۲.۱۳ نظرات، لایک و امتیازدهی (+22 more)
Cohesion: 0.15
Nodes (13): ۲. مدل داده و موجودیت‌ها, ۲.۱ کاربر (User), ۲.۱۰ پرداخت (Payment Types), ۲.۱۰.۱ پرداخت نوبت, ۲.۱۰.۲ پرداخت اشتراک, ۲.۱۱ پروفایل بیمار (Profile), ۲.۱۲ وبلاگ (Blog), ۲.۳ دکتر (Doctor) (+5 more)
### Community 208 - "Community 208"
Cohesion: 0.23
@@ -1576,8 +1600,8 @@ Cohesion: 0.23
Nodes (5): PaymentRepository, Appointment, ManagerRegistry, Payment, User
### Community 225 - "Community 225"
Cohesion: 0.30
Nodes (6): AbstractAuthenticator, Passport, PasswordAuthenticator, Request, Response, TokenInterface
Cohesion: 0.36
Nodes (3): DoctorServiceController, JsonResponse, Request
### Community 226 - "Community 226"
Cohesion: 0.15
@@ -1652,8 +1676,8 @@ Cohesion: 0.28
Nodes (4): SubscriptionService, ClinicSubscription, Payment, SubscriptionPlan
### Community 245 - "Community 245"
Cohesion: 0.05
Nodes (37): formatDate(), formatDateTime(), ALL_STATUSES, AppointmentDetailPage(), timeOf(), Claim, ClaimItem, DebtRow (+29 more)
Cohesion: 0.10
Nodes (13): Claim, ClaimItem, DebtRow, InsuranceOption, KIND_LABEL, STATUS_FILTERS, STATUS_META, IbanItem (+5 more)
### Community 246 - "Community 246"
Cohesion: 0.17
@@ -1692,8 +1716,8 @@ Cohesion: 0.18
Nodes (11): 1. 🔵 `POST` refresh token, 1. احراز هویت (Authentication), 2. 🟢 `GET` X-CSRF-Token, 3. 🟢 `GET` user info 🆕, Request Body, بخش دوم — مستند کامل API, خطاهای عمومی, فهرست مطالب (+3 more)
### Community 255 - "Community 255"
Cohesion: 0.11
Nodes (19): GET /api/v1/appointment-settings/slots — دریافت اسلات‌های خالی, GET /api/v1/categories/cities — شهرها, GET /api/v1/categories/states — استان‌ها, GET /api/v1/doctors/{id}, GET /api/v1/doctors/{id}/insurances, GET /api/v1/representations/{id}, GET /oauth/userinfo — اطلاعات کاربر (سازگار با دروپال), POST /api/v1/appointment-settings/holidays — ثبت تعطیلی (فقط ادمین) (+11 more)
Cohesion: 0.18
Nodes (11): GET /api/v1/doctors/{id}, GET /api/v1/doctors/{id}/insurances, GET /api/v1/representations/{id}, GET /oauth/userinfo — اطلاعات کاربر (سازگار با دروپال), POST /api/v1/representations/{id}/bank-accounts, POST /oauth/token — تجدید توکن (Refresh), POST /oauth/token — ورود به سیستم, Task-02: احراز هویت (Authentication) (+3 more)
### Community 256 - "Community 256"
Cohesion: 0.29
@@ -1768,16 +1792,16 @@ Cohesion: 0.21
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
### Community 275 - "Community 275"
Cohesion: 0.22
Nodes (3): AppException, SlotTakenException, RuntimeException
Cohesion: 0.24
Nodes (4): MellatGateway, MellatGatewayTest, ErrorCodesTest, TestCase
### Community 276 - "Community 276"
Cohesion: 0.20
Nodes (9): Architecture Audit — ClinicPro Symfony 7 Migration, Architecture Score (بعد از اصلاحات), Architecture Violations, Executive Summary, Final Verdict (بعد از اصلاحات), Missing Requirements (کامل), اصلاحات اعمال‌شده (بعد از Audit), دلیل تصمیم (+1 more)
### Community 277 - "Community 277"
Cohesion: 0.08
Nodes (23): GET /api/v1/sms/balance — موجودی حساب پیامک, POST /api/v1/sms/queue — افزودن به صف, بخش اول — مستند محصول (PRD), ثبت‌نام دکتر — از طریق نماینده, ثبت‌نام دکتر — از طریق کلینیک, ثبت‌نام دکتر — مستقل, فهرست کلی, کلینیک پرو — مستند جامع فنی و API (+15 more)
Cohesion: 0.20
Nodes (9): بخش اول — مستند محصول (PRD), فهرست کلی, کلینیک پرو — مستند جامع فنی و API, ۵. فهرست مشکلات شناسایی‌شده و اصلاحات لازم, ۶. پیوست, ۶.۱ دیاگرام وضعیت نوبت, ۶.۲ جریان کمیسیون, ۶.۳ بررسی محدودیت منشی (+1 more)
### Community 279 - "Community 279"
Cohesion: 0.20
@@ -1868,8 +1892,8 @@ Cohesion: 0.42
Nodes (3): PreRegistrationController, JsonResponse, Request
### Community 301 - "Community 301"
Cohesion: 0.09
Nodes (20): FreeVisitPrice(), Pricing, rialToToman(), tomanToRial(), IbanItem, RepMe, RepresentationSettlementPage(), RepSummary (+12 more)
Cohesion: 0.04
Nodes (45): FreeVisitPrice(), Pricing, usePaymentConfig(), formatDateTime(), formatRial(), rialToToman(), tomanToRial(), InsurancePricingPage() (+37 more)
### Community 302 - "Community 302"
Cohesion: 0.12
@@ -1880,8 +1904,8 @@ Cohesion: 0.12
Nodes (16): آماده‌سازی پروژه ClinicPro برای دیپلوی روی Coolify با Docker Compose, زمینه, فایل‌های مرتبط, نکات مهم (محدودیت‌ها و edge caseها), هدف, وظایف, ۱. ساخت `Dockerfile` چندمرحله‌ای, ۱۰. ساخت راهنمای `docs/deploy/coolify.md` (+8 more)
### Community 304 - "Community 304"
Cohesion: 0.04
Nodes (56): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 32. 🔵 `POST` post, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 35. 🟡 `PATCH` patch, 36. 🟢 `GET` get my rate, 37. 🔵 `POST` post (+48 more)
Cohesion: 0.22
Nodes (9): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 6. کلینیک (Clinic), هدرهای اضافی, پارامترهای Query, پاسخ‌ها, پاسخ‌ها (+1 more)
### Community 306 - "Community 306"
Cohesion: 0.07
@@ -1927,6 +1951,10 @@ Nodes (4): SubscriptionPeriodRepository, ManagerRegistry, SubscriptionPeriod, Su
Cohesion: 0.31
Nodes (3): SubscriptionPlanRepository, ManagerRegistry, SubscriptionPlan
### Community 318 - "Community 318"
Cohesion: 0.33
Nodes (4): PatientService, Appointment, PatientRecord, PatientSession
### Community 319 - "Community 319"
Cohesion: 0.39
Nodes (3): UserRepository, ManagerRegistry, User
@@ -2116,8 +2144,8 @@ Cohesion: 0.29
Nodes (7): Authentication, Authorization, Input Validation, Logging Security, Rate Limiting, Secrets Management, ۷. تحلیل امنیت
### Community 371 - "Community 371"
Cohesion: 0.39
Nodes (4): FinancialBreakdown, FinancialBreakdownRepository, ManagerRegistry, Payment
Cohesion: 0.15
Nodes (6): UniqueConstraintsTest, FinancialBreakdown, FinancialBreakdownRepository, FinancialBreakdownIntegrityTest, ManagerRegistry, Payment
### Community 372 - "Community 372"
Cohesion: 0.11
@@ -2219,10 +2247,6 @@ Nodes (6): ClaimAmountBoundsTest, ClaimsListNPlusOneTest, ClaimItem, Claim, Doct
Cohesion: 0.22
Nodes (7): initiate(), refund(), reverse(), verify(), PaymentInitResult, PaymentRefundResult, PaymentVerifyResult
### Community 399 - "Community 399"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260611075829
### Community 401 - "Community 401"
Cohesion: 0.12
Nodes (15): `lib/utils.ts`, `SettingsPage.tsx` (ورودی‌ها ریال ذخیره می‌شوند), زمینه, فایل‌های مرتبط, مشکل / هدف, نمونهٔ نمایش (Subscription), نکات مهم, واحد پول = تومان در پنل ادمین (نمایش ÷۱۰ / ورودی ×۱۰) — ذخیره و درگاه ریال می‌ماند (+7 more)
@@ -2239,6 +2263,10 @@ Nodes (11): KavehNegarProvider, MessageBusInterface, MockObject, SmsService, Sms
Cohesion: 0.17
Nodes (11): دیپلوی ClinicPro روی Coolify (Docker Compose), رفع اشکال, مراحل دیپلوی, معماری دیپلوی, نکات عملیاتی, چند دامنه فرانت‌اند (مهم), ۱. ساخت Resource در Coolify, ۲. اختصاص دامنه (+3 more)
### Community 421 - "Community 421"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609132009, Schema, Version20260619062912
### Community 424 - "Community 424"
Cohesion: 0.33
Nodes (5): SecretaryController, DoctorSecretary, JsonResponse, Request, User
@@ -2256,8 +2284,8 @@ Cohesion: 0.11
Nodes (18): [F10] راهنمای کهنه در `CLAUDE.md`: endpoint `categorys/{bundle}` منتقل شده, [F11] داشبورد دکتر `GET /api/v1/dashboard/doctor` همیشه 500 (فیلد ناموجود در DQL) — ✅ رفع شد, [F1] phpstan: مقایسهٔ همیشه‌درست در محاسبهٔ estimated SMS — ✅ رفع شد, [F2] تست‌های PHPUnit به API خارجی Kavenegar درخواست واقعی می‌زنند, [F3] دیتابیس تست seed نشده — فقط کاربر ادمین وجود دارد, [F4] اسکریپت seeder `create_test_users.php` وجود ندارد, [F5] ادمین با JWT معتبر به `/api/doc` (Swagger UI) دسترسی ندارد (401), [F6] ناسازگاری کدهای خطا بین دامنه‌ها (+10 more)
### Community 452 - "Community 452"
Cohesion: 0.06
Nodes (31): cn(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile(), sanitizeMobileInput(), toDate(), toEnglishDigits() (+23 more)
Cohesion: 0.05
Nodes (37): ApiResponse, cn(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile(), sanitizeMobileInput(), toDate() (+29 more)
### Community 456 - "Community 456"
Cohesion: 0.15
@@ -2268,8 +2296,8 @@ Cohesion: 0.47
Nodes (6): formatPersianDate(), gToJ(), jFirstDayOfWeek(), PersianDateInput(), todayGregorian(), toPersianNums()
### Community 459 - "Community 459"
Cohesion: 0.40
Nodes (5): API موجود (نیاز به تغییر ندارند), اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.33
Nodes (6): API موجود (نیاز به تغییر ندارند), اپیک‌ها, اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 460 - "Community 460"
Cohesion: 0.33
@@ -2348,8 +2376,8 @@ Cohesion: 0.40
Nodes (5): Errors, Notes, POST `/api/v1/notification-mobile/request-otp`, Request Body, Response `200`
### Community 482 - "Community 482"
Cohesion: 0.38
Nodes (3): RepositoryClassMappingTest, KernelTestCase, DbLoggerTest
Cohesion: 0.47
Nodes (3): CommissionService, Payment, Representation
### Community 483 - "Community 483"
Cohesion: 0.40
@@ -2371,6 +2399,10 @@ Nodes (5): Errors, GET `/api/v1/representation/dashboard/summary`, GET `/api/v1/
Cohesion: 0.40
Nodes (5): Admin Endpoints, GET /api/v1/admin/sms/settings/review, GET /api/v1/admin/sms/wallet-report, POST /api/v1/admin/sms/settings/{id}/approve, POST /api/v1/admin/sms/settings/{id}/reject
### Community 490 - "Community 490"
Cohesion: 0.25
Nodes (8): ثبت‌نام دکتر — از طریق نماینده, ثبت‌نام دکتر — از طریق کلینیک, ثبت‌نام دکتر — مستقل, ۱. معرفی محصول, ۱.۱ نقش‌های سیستم, ۱.۲ فلوهای عملیاتی اصلی, ۱.۳ پلن‌های اشتراک, ۱.۴ سیستم پیامک
### Community 491 - "Community 491"
Cohesion: 0.12
Nodes (15): `Modal.tsx` (بدون Portal), `PersianCalendar.tsx` (buttonها بدون `type`) — نمونه‌ها, باگ ۱ — علت, باگ ۲ — علت, رفع دو باگ Modal و تقویم شمسی در پنل ادمین, زمینه, فایل‌های مرتبط, مشکل / هدف (+7 more)
@@ -2404,8 +2436,8 @@ Cohesion: 0.40
Nodes (5): addMinutes(), calcSlotCount(), hasOverlap(), parseMinutes(), SessionEditor()
### Community 500 - "Community 500"
Cohesion: 0.33
Nodes (6): API موجود (نیاز به endpoint جدید ندارد), اپیک‌ها, اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.40
Nodes (5): API موجود (نیاز به endpoint جدید ندارد), اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 501 - "Community 501"
Cohesion: 0.40
@@ -2720,8 +2752,8 @@ Cohesion: 0.67
Nodes (3): نقاط ضعف, نقاط قوت, ۶. تحلیل API Design
### Community 606 - "Community 606"
Cohesion: 0.39
Nodes (3): ClinicStaffRepository, ClinicStaff, ManagerRegistry
Cohesion: 0.25
Nodes (8): ۲.۲ انواع دسته‌بندی (Category Types), ۲.۲.۱ تگ (Tag), ۲.۲.۲ استان (State), ۲.۲.۳ شهر (City), ۲.۲.۴ بیمه پایه (Basic Insurance), ۲.۲.۵ بیمه مکمل (Supplementary Insurance), ۲.۲.۶ تخصص دکتر (Doctor Specialty), ۲.۲.۷ خدمات دکتر (Doctor Services)
### Community 607 - "Community 607"
Cohesion: 0.35
@@ -2804,8 +2836,8 @@ Cohesion: 0.38
Nodes (5): MyAppointmentsController, Doctor, JsonResponse, Request, User
### Community 656 - "Community 656"
Cohesion: 0.53
Nodes (3): SessionServiceRepository, ManagerRegistry, SessionService
Cohesion: 0.43
Nodes (3): SmsTemplateRepository, SmsTemplate, ManagerRegistry
### Community 657 - "Community 657"
Cohesion: 0.20
@@ -2819,6 +2851,10 @@ Nodes (3): GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`, Path Parameters
Cohesion: 0.31
Nodes (4): ClinicInvitationService, Clinic, ClinicDoctorInvitation, User
### Community 661 - "Community 661"
Cohesion: 0.39
Nodes (3): TagRepository, ManagerRegistry, Tag
### Community 662 - "Community 662"
Cohesion: 0.29
Nodes (7): Application Logs, DELETE `/api/v1/admin/logs`, GET `/api/v1/admin/logs`, Log Retention, Query Parameters, Response `200`, Response `200`
@@ -2827,10 +2863,6 @@ Nodes (7): Application Logs, DELETE `/api/v1/admin/logs`, GET `/api/v1/admin/log
Cohesion: 0.38
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
### Community 666 - "Community 666"
Cohesion: 0.40
Nodes (5): Errors, Path Parameters, POST `/api/v1/doctor/invitation/{invUuid}/respond`, Request Body, Response `200`
### Community 667 - "Community 667"
Cohesion: 0.40
Nodes (5): DELETE `/api/v1/billing/tenant-insurances/{uuid}`, GET `/api/v1/billing/tenant-insurances`, PATCH `/api/v1/billing/tenant-insurances/{uuid}`, POST `/api/v1/billing/tenant-insurances`, TenantInsurance — قراردادهای بیمه‌ی tenant (فاز ۱ سیستم صورتحساب)
@@ -2879,6 +2911,10 @@ Nodes (5): GET `/api/v1/admin/comments`, GET `/api/v1/admin/rates`, Query Parame
Cohesion: 0.50
Nodes (4): Errors, PATCH `/api/v1/admin/insurance/{id}`, Path Parameters, Response `200`
### Community 688 - "Community 688"
Cohesion: 0.33
Nodes (3): Closure, CorsRegexEnvProcessor, EnvVarProcessorInterface
### Community 689 - "Community 689"
Cohesion: 0.50
Nodes (4): PUT `/api/v1/insurance-pricing`, Request Body, Response `200`, خطاها
@@ -2907,24 +2943,116 @@ Nodes (3): DELETE `/api/v1/sms/template/{uuid}`, Errors, Response `200`
Cohesion: 0.67
Nodes (3): Errors, POST `/api/v1/sms/send` — ⛔ غیرفعال (Deprecated), Response `422` (همیشه)
### Community 697 - "Community 697"
Cohesion: 0.43
Nodes (3): SmsMessageController, JsonResponse, Request
### Community 698 - "Community 698"
Cohesion: 0.43
Nodes (3): InvoiceService, Invoice, PatientSession
### Community 699 - "Community 699"
Cohesion: 0.33
Nodes (6): GET /api/v1/sms/balance — موجودی حساب پیامک, POST /api/v1/sms/queue — افزودن به صف, ۴. سیستم حساب و صف پیامک, ۴.۱ حساب پیامک (SMS Account), ۴.۲ صف پیامک (SMS Queue), ۴.۳ API پیامک
### Community 701 - "Community 701"
Cohesion: 0.53
Nodes (4): Money, BillingCalculator, CoverageRule, ShareBreakdown
### Community 702 - "Community 702"
Cohesion: 0.53
Nodes (3): PaymentLog, PaymentLogRepository, ManagerRegistry
### Community 703 - "Community 703"
Cohesion: 0.40
Nodes (5): 32. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 704 - "Community 704"
Cohesion: 0.40
Nodes (5): 35. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 705 - "Community 705"
Cohesion: 0.40
Nodes (5): 37. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 706 - "Community 706"
Cohesion: 0.40
Nodes (5): ۲.۸ تنظیمات نوبت (Appointment Settings), ۲.۸.۱ برنامه هفتگی (Weekly Schedule), ۲.۸.۲ تعطیلات (Holidays), ۲.۸.۳ لغو تعطیل (Date Override), ۲.۸.۴ الگوریتم محاسبه اسلات‌های خالی
### Community 708 - "Community 708"
Cohesion: 0.40
Nodes (3): Props, StatTone, TONE
### Community 709 - "Community 709"
Cohesion: 0.50
Nodes (4): DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Path Parameters, Response `204`
### Community 710 - "Community 710"
Cohesion: 0.50
Nodes (4): 39. 🟡 `PATCH` Comment confirmation, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 711 - "Community 711"
Cohesion: 0.50
Nodes (4): 40. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 712 - "Community 712"
Cohesion: 0.50
Nodes (4): 44. 🟢 `GET` list comment, هدرهای اضافی, پارامترهای Query, پاسخ‌ها
### Community 713 - "Community 713"
Cohesion: 0.50
Nodes (4): 45. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 714 - "Community 714"
Cohesion: 0.50
Nodes (4): 46. 🟡 `PATCH` patch, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 715 - "Community 715"
Cohesion: 0.50
Nodes (4): GET /api/v1/appointment-settings/slots — دریافت اسلات‌های خالی, POST /api/v1/appointment-settings/holidays — ثبت تعطیلی (فقط ادمین), POST /api/v1/appointment-settings/overrides — ثبت Override توسط دکتر, Task-09: API تنظیمات نوبت
### Community 716 - "Community 716"
Cohesion: 0.50
Nodes (4): GET /api/v1/categories/cities — شهرها, GET /api/v1/categories/states — استان‌ها, Task-08: API دسته‌بندی‌ها, سایر Endpoint های دسته‌بندی — الزامی
### Community 717 - "Community 717"
Cohesion: 0.50
Nodes (4): ۲.۱۳ نظرات، لایک و امتیازدهی, ۲.۱۳.۱ نظر (Comment), ۲.۱۳.۲ لایک (Like), ۲.۱۳.۳ امتیاز (Rate)
### Community 721 - "Community 721"
Cohesion: 0.67
Nodes (3): 34. 🔵 `POST` image logo, هدرهای اضافی, پاسخ‌ها
### Community 722 - "Community 722"
Cohesion: 0.67
Nodes (3): 36. 🟢 `GET` get my rate, هدرهای اضافی, پاسخ‌ها
### Community 723 - "Community 723"
Cohesion: 0.67
Nodes (3): 42. 🔴 `DELETE` delete, هدرهای اضافی, پاسخ‌ها
### Community 724 - "Community 724"
Cohesion: 0.67
Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخ‌ها
## Knowledge Gaps
- **3916 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+3911 more)
- **3927 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+3922 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **137 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **140 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `BaseController` connect `Community 108` to `Community 4`, `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 654`, `Community 655`, `Community 15`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 424`, `Community 169`, `Community 300`, `Community 175`, `Community 177`, `Community 58`, `Community 59`, `Community 63`, `Community 64`, `Community 75`, `Community 77`, `Community 607`, `Community 227`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.027) - this node is a cross-community bridge._
- **Why does `ApiTestCase` connect `Community 86` to `Community 397`, `Community 661`, `Community 22`, `Community 535`, `Community 541`, `Community 41`, `Community 688`, `Community 562`, `Community 565`, `Community 573`, `Community 574`, `Community 318`, `Community 575`, `Community 594`, `Community 352`, `Community 609`, `Community 484`, `Community 618`, `Community 497`?**
- **Why does `BaseController` connect `Community 108` to `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 654`, `Community 655`, `Community 15`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 424`, `Community 300`, `Community 697`, `Community 58`, `Community 59`, `Community 63`, `Community 64`, `Community 75`, `Community 77`, `Community 607`, `Community 225`, `Community 227`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.029) - this node is a cross-community bridge._
- **Why does `ApiTestCase` connect `Community 86` to `Community 352`, `Community 609`, `Community 541`, `Community 707`, `Community 484`, `Community 41`, `Community 618`, `Community 397`, `Community 497`, `Community 594`, `Community 562`, `Community 82`, `Community 565`, `Community 371`, `Community 535`, `Community 573`, `Community 574`, `Community 575`?**
_High betweenness centrality (0.017) - this node is a cross-community bridge._
- **Why does `AppointmentRepository` connect `Community 119` to `Community 53`?**
- **Why does `Version20260705070546` connect `Community 646` to `Community 421`?**
_High betweenness centrality (0.017) - this node is a cross-community bridge._
- **What connects `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps` to the rest of the system?**
_3916 weakly-connected nodes found - possible documentation gaps or missing edges._
_3927 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05200501253132832 - nodes in this community are weakly interconnected._
_Cohesion score 0.05387205387205387 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.028985507246376812 - nodes in this community are weakly interconnected._
- **Should `Community 2` be split into smaller, more focused modules?**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docker_gen_cors_env_php", "label": "gen-cors-env.php", "file_type": "code", "source_file": "docker/gen-cors-env.php", "source_location": "L1"}], "edges": [], "raw_calls": []}
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1922 -1207
View File
File diff suppressed because it is too large Load Diff
+19 -4
View File
@@ -2240,8 +2240,8 @@
"semantic_hash": ""
},
"config/packages/nelmio_cors.yaml": {
"mtime": 1782933581.6906326,
"ast_hash": "c552214796d17d0d2a94154346b2afa8",
"mtime": 1783423349.9111028,
"ast_hash": "cf61699b7fc9ff8921d28d0c735361a1",
"semantic_hash": ""
},
"config/packages/property_info.yaml": {
@@ -2995,8 +2995,8 @@
"semantic_hash": ""
},
"docker/gen-cors-env.php": {
"mtime": 1782410159.2191367,
"ast_hash": "aa2b2af54e520f8e6d446dd328f6d54d",
"mtime": 1783423497.5513937,
"ast_hash": "bf59fa3835df846b483890f63a369755",
"semantic_hash": ""
},
"docker/healthcheck.sh": {
@@ -3703,5 +3703,20 @@
"mtime": 1783066913.0879276,
"ast_hash": "074cb84109dd212089ba2b5397e9b3ed",
"semantic_hash": ""
},
"src/Shared/DependencyInjection/CorsRegexEnvProcessor.php": {
"mtime": 1783423339.918379,
"ast_hash": "1fe5ad4d178ba633fe0a1c1d7c830e2e",
"semantic_hash": ""
},
"tests/Shared/CorsRegexEnvProcessorTest.php": {
"mtime": 1783423530.411102,
"ast_hash": "67be8d92badcc2506649af90503cfc0c",
"semantic_hash": ""
},
".claude/prompt/cors-regex-from-hosts-coolify-safe.md": {
"mtime": 1783423233.682262,
"ast_hash": "21ec0778cfc1438f1f8a55c8c0adf55f",
"semantic_hash": ""
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Shared\DependencyInjection;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
/**
* Builds the nelmio CORS origin regex from a comma-separated host list
* (ALLOWED_FRONTEND_HOSTS). That env is Coolify-safe (no `$` / `\`), whereas a
* ready-made regex env is mangled by Coolify's variable interpolation and reaches
* the container broken so we assemble the regex in PHP instead.
*
* Usage: %env(cors_regex:ALLOWED_FRONTEND_HOSTS)%
*/
final class CorsRegexEnvProcessor implements EnvVarProcessorInterface
{
public function getEnv(string $prefix, string $name, \Closure $getEnv): string
{
$raw = (string) $getEnv($name);
$hosts = array_values(array_filter(array_map('trim', explode(',', $raw))));
if ($hosts === []) {
// Match nothing rather than emit an empty (accept-all/reject-all-ambiguous) pattern.
return '(?!)';
}
$alts = implode('|', array_map(
static fn (string $h): string => preg_quote($h, '#'),
$hosts
));
// Same shape as the previous CORS_ALLOW_ORIGIN: optional subdomain, http or
// https (dev localhost uses http), optional port, anchored.
return '^https?://([a-z0-9-]+\.)*(' . $alts . ')(:[0-9]+)?$';
}
public static function getProvidedTypes(): array
{
return ['cors_regex' => 'string'];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Tests\Shared;
use App\Shared\DependencyInjection\CorsRegexEnvProcessor;
use PHPUnit\Framework\TestCase;
class CorsRegexEnvProcessorTest extends TestCase
{
private function regexFor(string $hosts): string
{
$processor = new CorsRegexEnvProcessor();
return $processor->getEnv('cors_regex', 'ALLOWED_FRONTEND_HOSTS', fn () => $hosts);
}
/** رجکس ساخته‌شده با delimiter نلمیو (`#`) کامپایل و روی origin تست می‌شود. */
private function originMatches(string $regex, string $origin): bool
{
return (bool) preg_match('#' . $regex . '#i', $origin);
}
public function testAllowsListedHostsHttpsAndSubdomains(): void
{
$regex = $this->regexFor('yasuj-nobat.ir,nobat724.com');
$this->assertTrue($this->originMatches($regex, 'https://yasuj-nobat.ir'));
$this->assertTrue($this->originMatches($regex, 'https://www.nobat724.com'));
}
public function testAllowsHttpAndPortForLocalDev(): void
{
$regex = $this->regexFor('localhost,yazd-nobat.localhost');
$this->assertTrue($this->originMatches($regex, 'http://yazd-nobat.localhost:3000'));
$this->assertTrue($this->originMatches($regex, 'http://localhost:8080'));
}
public function testRejectsUnlistedAndLookalikeHosts(): void
{
$regex = $this->regexFor('yasuj-nobat.ir');
$this->assertFalse($this->originMatches($regex, 'https://evil.com'));
// انکورِ انتها: دامنه‌ای که با هاست مجاز شروع/تمام نشود نباید مچ شود.
$this->assertFalse($this->originMatches($regex, 'https://yasuj-nobat.ir.evil.com'));
$this->assertFalse($this->originMatches($regex, 'https://notyasuj-nobat.ir'));
}
public function testDotIsEscapedNotWildcard(): void
{
$regex = $this->regexFor('nobat724.com');
// نقطه نباید به‌عنوان wildcard عمل کند.
$this->assertFalse($this->originMatches($regex, 'https://nobat724xcom'));
}
public function testEmptyListMatchesNothing(): void
{
$regex = $this->regexFor('');
$this->assertFalse($this->originMatches($regex, 'https://yasuj-nobat.ir'));
$this->assertFalse($this->originMatches($regex, 'https://anything.com'));
}
public function testProvidedType(): void
{
$this->assertSame(['cors_regex' => 'string'], CorsRegexEnvProcessor::getProvidedTypes());
}
}