Implement ALTCHA captcha service with challenge generation and solution verification

- Added AltchaService class for managing ALTCHA captcha challenges and solutions.
- Created CaptchaController to handle API requests for generating challenges.
- Introduced CaptchaGuard for validating captcha solutions on public endpoints.
- Developed unit tests for AltchaService to ensure challenge creation and solution verification functionality.
- Implemented integration tests for the Captcha API endpoint and captcha bypass behavior when disabled.
- Added documentation for the Captcha API in the corresponding markdown file.
This commit is contained in:
hamed
2026-07-10 10:31:59 +03:30
parent 11efed4100
commit 10b0743d9a
43 changed files with 5586 additions and 1554 deletions
@@ -0,0 +1,179 @@
# یکپارچه‌سازی ALTCHA (کپچای Self-Hosted، Proof-of-Work) روی endpointهای عمومی
## پروژه
`clinicpro` (backend + پنل ادمین React).
> **Cross-repo:** سایت عمومی `nobat724_front` هم همان endpointهای عمومی را مصرف می‌کند (send-code / register / rate / comment / pre-registration). این پرامپت **قرارداد API کپچا** را در backend می‌سازد؛ سپس یک پرامپت جدا در `nobat724_front` برای نصب web-component و ضمیمه‌کردن `altcha` به بدنه‌ی همان درخواست‌ها لازم است. مسیر challenge و شکل payload که در این فایل تعریف می‌شود، همان است که سایت عمومی باید استفاده کند.
## زمینه
کاربر خواسته «به همه‌ی فرم‌های Symfony با ALTCHA کپچا اضافه شود» و صریحاً هر SaaS خارجی (Google/Cloudflare/hCaptcha/Friendly) را رد کرده — چون کاربران داخل ایران‌اند. انتخاب **ALTCHA** درست است: خودمیزبان، بدون تصویر، بدون تایپ، بدون CDN، Proof-of-Work سمت مرورگر.
**اما یک تصحیح معماری مهم:** این پروژه Symfony Forms / Twig برای فرم‌های کاربری **ندارد**. `clinicpro` یک REST API با envelope‌ی JSON است و کلاینت‌ها React SPA (پنل ادمین) و Next.js (`nobat724_front`) هستند. پس «Form Type / Form Extension / Form Theme / Twig block» موضوعیت ندارد. معادل درست در این معماری:
- یک endpoint برای صدور **challenge امضاشده**،
- یک **verifier سرویس** که payload حل‌شده‌ی ALTCHA را سمت سرور اعتبارسنجی می‌کند،
- اعمال verifier روی endpointهای POST عمومی که واقعاً قربانی اسپم/بات‌اند.
هیچ‌کدام از فرم‌های ادمین (پشت JWT + `ROLE_*`) نیازی به کپچا ندارند؛ کپچا فقط روی سطح **عمومی و بدون احراز هویت** معنی دارد.
## مشکل / هدف
افزودن ALTCHA به‌صورت یک ماژول مرکزی و قابل‌استفاده‌مجدد، که با یک خط (`$this->altcha->assertValid($request)`) به هر endpoint عمومی اضافه شود؛ در `dev`/`test` غیرفعال و در `prod` فعال؛ با محافظت در برابر replay (استفاده‌ی یک‌باره‌ی هر challenge).
## endpointهای عمومی که باید محافظت شوند
از `config/packages/security.yaml` → firewall `public_endpoints` (خط ۳۵). فقط POSTهای ارسال‌کننده‌ی داده/هزینه‌زا:
| Endpoint | Controller | چرا کپچا |
|---|---|---|
| `POST /api/v1/user/send-code` | `src/Auth/Controller/AuthController.php::sendCode` (خط ۱۳۶) | **بحرانی** — هر تماس یک پیامک واقعی + هزینه می‌فرستد |
| `POST /api/v1/user/register` | `AuthController::register` (خط ۲۷۶) | ساخت اکانت انبوه |
| `POST /api/v1/user/otp-login` | `AuthController::otpLogin` (خط ۳۷۲) | تلاش خودکار ورود |
| `POST /api/v1/user/reset-password` | `AuthController::resetPassword` (خط ۳۹۶) | سوءاستفاده از بازیابی |
| `POST /api/v1/pre-registration` | `src/Auth/Controller/PreRegistrationController.php` (خط ۴۰) | اسپم درخواست ثبت‌نام |
| `POST /api/v1/rate` | `src/Rating/Controller/RatingController.php` (خط ۶۷) | امتیاز جعلی انبوه |
| `POST /api/v1/comment` | `RatingController.php` (خط ۲۱۴) | اسپم نظر |
> `verify-code` خودش با rate-limit و uuid محافظت است و کپچا اضافه لازم ندارد (تجربه‌ی کاربر را خراب می‌کند). اگر لازم شد، فقط `send-code` را کپچا بزن — بقیه اختیاری با فلگ.
## وضعیت فعلی
الگوی هر endpoint عمومی امروز: rate-limit با `RateLimiterFactory` (Redis)، سپس `json_decode` بدنه، سپس `$this->error(...)`. نمونه‌ی واقعی از `AuthController::sendCode`:
```php
public function sendCode(Request $request): JsonResponse
{
$limiter = $this->sendCodeLimiter->create($request->getClientIp() ?? 'unknown');
if (!$limiter->consume(1)->isAccepted()) {
return $this->error(ErrorCodes::ERR_RATE_LIMIT_001, ErrorCodes::message(ErrorCodes::ERR_RATE_LIMIT_001), 429);
}
$data = json_decode($request->getContent(), true) ?? [];
$mobile = trim($data['mobile'] ?? '');
// ... اعتبارسنجی موبایل ...
$uuid = $this->otpService->sendCode($mobile, $domain ?: null);
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
}
```
زیرساخت موجود که باید استفاده شود:
- **Redis** آماده است: `config/packages/cache.yaml``app: cache.adapter.redis`، `REDIS_URL=redis://redis:6379`. برای store یک‌باره‌ی challengeها از یک cache pool اختصاصی استفاده کن (نه ساخت اتصال دستی).
- همه‌ی controllerها `extends BaseController` و از `$this->error($code, $message, $status, $field?)` استفاده می‌کنند.
- کدهای خطا در `src/Shared/Constant/ErrorCodes.php` (پیام فارسی).
- Webpack Encore با entryهای `admin` و `home` (`webpack.config.js` خطوط ۱۱–۱۳). **AssetMapper در کار نیست** — دارایی‌ها با Encore بسته می‌شوند.
## وظایف
### ۱. نصب کتابخانه‌ی رسمی PHP
```bash
ddev exec composer require altcha-org/altcha
```
اگر نصب پکیج به هر دلیل ممکن نبود، معادل حداقلی طبق مستندات رسمی ALTCHA پیاده کن (HMAC-SHA256 روی `salt+number`، خروجی base64). ولی **اول پکیج رسمی را امتحان کن**.
### ۲. سرویس مرکزی — `src/Shared/Captcha/AltchaService.php` (namespace جدید `App\Shared\Captcha`)
مسئولیت‌ها:
- `createChallenge(): array` — تولید challenge امضاشده با HMAC-SHA256 و `%env(ALTCHA_HMAC_KEY)%`، `expires`، و `maxNumber = %env(int:ALTCHA_MAX_NUMBER)%`.
- `verifySolution(string $payloadBase64): bool` — decode payload، بررسی امضا (`algorithm`, `challenge`, `salt`, `signature`)، بررسی انقضا از داخل `salt` (پارامتر `?expires=`)، سپس **one-time**: کلید `altcha:used:<challenge>` را در cache pool اتمیک `get`/`save` کن؛ اگر قبلاً بوده → `false` (ضدِ replay). TTL = زمان باقیمانده تا انقضا.
- `enabled(): bool` — از `%env(bool:ALTCHA_ENABLED)%`.
تزریق: `CacheItemPoolInterface $altchaPool`, پارامترهای env. از `random_bytes` برای salt، `hash_hmac('sha256', ...)` برای امضا.
### ۳. Guard قابل‌استفاده‌مجدد — `src/Shared/Captcha/CaptchaGuard.php`
یک متد کوتاه که در هر controller صدا زده شود:
```php
public function assertValid(Request $request): void
{
if (!$this->altcha->enabled()) {
return; // dev/test یا ALTCHA_ENABLED=false
}
$payload = (string) (json_decode($request->getContent(), true)['altcha'] ?? '');
if ($payload === '' || !$this->altcha->verifySolution($payload)) {
throw new AppException(ErrorCodes::ERR_CAPTCHA_001, null, 422);
}
}
```
`AppException` توسط `ExceptionSubscriber` به `$this->error()` تبدیل می‌شود — پس نیازی به try/catch در controller نیست. یک کد خطای جدید `ERR_CAPTCHA_001` با پیام فارسی («تأیید امنیتی ناموفق بود، صفحه را تازه کنید») به `ErrorCodes.php` اضافه کن.
### ۴. Endpoint صدور challenge — عمومی
در یک controller جدید `src/Shared/Captcha/CaptchaController.php`:
```php
#[Route('/api/v1/altcha/challenge', methods: ['GET'])]
public function challenge(): JsonResponse
{
return new JsonResponse($this->altcha->createChallenge());
}
```
این مسیر را به `public_endpoints` در `config/packages/security.yaml` (خط ۳۵ pattern) اضافه کن: `api/v1/altcha/challenge`.
### ۵. اعمال Guard روی endpointها
در ابتدای هر یک از ۷ متد جدول بالا (بعد از rate-limit موجود، قبل از منطق):
```php
$this->captcha->assertValid($request);
```
`CaptchaGuard` را به constructor آن controllerها inject کن. **rate-limitهای موجود را حذف نکن** — کپچا مکمل آن‌هاست نه جایگزین.
### ۶. تنظیمات
`.env` (مقادیر پیش‌فرض؛ کلید واقعی در `.env.local`):
```dotenv
###> altcha ###
ALTCHA_ENABLED=false
ALTCHA_HMAC_KEY=change-me-in-env-local
ALTCHA_MAX_NUMBER=100000
ALTCHA_EXPIRE_SECONDS=300
###< altcha ###
```
`config/services.yaml`: bind پارامترها به `AltchaService`. یک cache pool اختصاصی در `config/packages/cache.yaml`:
```yaml
framework:
cache:
pools:
altcha.pool:
adapter: cache.adapter.redis
default_lifetime: 600
```
غیرفعال‌سازی محیطی: در `config/services_dev.yaml` و `config/packages/test/` مقدار `ALTCHA_ENABLED=false` تضمین شود؛ در `prod` مقدار از `.env.local` سرور `true`. (به‌جای اتکا به env، `enabled()` مستقیماً `ALTCHA_ENABLED` را می‌خواند تا رفتار صریح باشد.)
### ۷. Frontend پنل ادمین React (`assets/admin/`)
- نصب web-component محلی: `ddev exec yarn add altcha` (بدون CDN؛ Encore آن را bundle می‌کند).
- یک کامپوننت `assets/admin/components/ui/Altcha.tsx` که `<altcha-widget>` را با `challengeurl="/api/v1/altcha/challenge"` رندر می‌کند و مقدار حل‌شده را از event `verified` می‌گیرد.
- در فرم‌های عمومی ادمین (صفحه‌ی login عمومی اگر روی همین endpointها می‌رود) مقدار `altcha` را به بدنه‌ی درخواست در `lib/api.ts` ضمیمه کن. اگر پنل ادمین از این endpointهای عمومی استفاده نمی‌کند، این بخش را حداقلی نگه‌دار و در README مسیر افزودن را مستند کن.
> بیشتر مصرف‌کننده‌ی این endpointها `nobat724_front` است؛ سیم‌کشی کامل widget آنجا در پرامپت همتای frontend انجام می‌شود.
### ۸. تست
- **Unit** `tests/Shared/Captcha/AltchaServiceTest.php`: امضای معتبر تأیید شود؛ امضای دستکاری‌شده رد؛ challenge منقضی رد؛ استفاده‌ی دوم از همان challenge رد (replay).
- **Functional** `tests/Shared/Captcha/CaptchaFlowTest.php`: `GET /api/v1/altcha/challenge` ساختار (`algorithm/challenge/salt/signature/maxnumber`) برگرداند؛ با `ALTCHA_ENABLED=false` (پیش‌فرض test) endpointهای عمومی بدون `altcha` هم ۲۰۰ بدهند؛ سپس با فعال‌سازی موقت سرویس (mock/override) نبودِ `altcha` → ۴۲۲ با `ERR_CAPTCHA_001`.
- از `ApiTestCase` ارث ببر؛ الگوی موجود `tests/Admin/AdminLogsTest.php` را دنبال کن.
### ۹. مستندات
- `docs/api/captcha.md` جدید: مسیر challenge، شکل payload، کد خطای `ERR_CAPTCHA_001`، لیست endpointهای محافظت‌شده.
- در `docs/api/auth.md`، `docs/api/rating.md`، و مستند pre-registration: به هر endpoint یک نکته اضافه کن که در `prod` هدر/فیلد `altcha` الزامی است.
- `README` بخش ALTCHA: نصب، env، فعال/غیرفعال، افزودن به endpoint جدید (`$this->captcha->assertValid($request)`)، تغییر difficulty (`ALTCHA_MAX_NUMBER`)، Troubleshooting (کلید HMAC ناهماهنگ بین challenge و verify، ساعت سرور/انقضا، Redis در دسترس نبودن).
## نکات مهم
- **این پروژه Twig/FormType برای فرم‌های کاربری ندارد** — پیاده‌سازی API-محور است (challenge endpoint + verifier)، نه Form Theme. اگر جایی صفحه‌ی Twig عمومی با فرم واقعی بود (`templates/public/`)، همان‌جا web-component را مستقیم بگذار؛ ولی فرض پیش‌فرض API است.
- کپچا فقط روی `public_endpoints`؛ **هرگز روی endpointهای پشت JWT/`ROLE_*`** (تجربه‌ی ادمین را خراب و بی‌فایده است).
- One-time بودن challenge **الزامی** است؛ بدون آن replay ممکن می‌شود. حتماً از Redis pool اتمیک استفاده کن.
- Secret/HMAC key هرگز به client نرود؛ فقط challenge امضاشده و salt عمومی‌اند.
- rate-limitهای موجود دست‌نخورده بمانند؛ کپچا لایه‌ی مکمل است.
- envelope پاسخ خطا باید همان `$this->error()` استاندارد BaseController بماند (`{ success:false, errors:[{code,message}] }`).
- بعد از تغییر controllerها و افزودن endpoint، طبق قانون ثابت پروژه فایل‌های `docs/api/*` را در همین session به‌روز کن.
- بعد از تغییر کد: `ddev exec php bin/console cache:clear` (route جدید) و `ddev exec php bin/phpunit` و `ddev exec yarn dev`.
+3
View File
@@ -9,3 +9,6 @@ DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.0&charset=utf8mb4"
# JWT — generated keypair is shared with dev; passphrase from .env is fine.
# Redis cache/messenger use the same ddev redis; tests don't depend on it.
# ALTCHA off in tests so public endpoints stay drivable without solving PoW.
ALTCHA_ENABLED=false
+47
View File
@@ -812,3 +812,50 @@ clinic-pro-symfony/
> CSS فرانت‌اند از کلاس‌های template اختصاصی استفاده می‌کند (نه Tailwind).
> بعد از هر تغییر backend: `cache:clear`
> بعد از هر تغییر entity: `migrations:diff` سپس `migrations:migrate`
---
## 🛡️ ALTCHA — کپچای خودمیزبان (ضدِ اسپم/بات)
کپچای Proof-of-Work مبتنی بر [ALTCHA](https://altcha.org) — بدون تصویر، بدون تایپ، بدون سرویس خارجی (مناسب کاربران داخل ایران). فقط روی endpointهای **عمومی و بدون احراز هویت** اعمال می‌شود.
### نصب (انجام‌شده)
- بک‌اند: `composer require altcha-org/altcha` (پکیج رسمی PHP، از V1 API استفاده می‌شود).
- فرانت‌اند: `yarn add altcha` (web-component محلی، بدون CDN؛ Encore آن را bundle می‌کند).
### تنظیمات (`.env` / برای prod در `.env.local`)
| متغیر | پیش‌فرض | توضیح |
|---|---|---|
| `ALTCHA_ENABLED` | `false` | در prod روی `true`؛ در dev/test غیرفعال بماند |
| `ALTCHA_HMAC_KEY` | `change-me-in-env-local` | کلید امضای سرور — **حتماً در prod عوض شود** و مخفی بماند |
| `ALTCHA_MAX_NUMBER` | `100000` | سقف اعداد PoW = **سختی**؛ بالاتر = سنگین‌تر برای مرورگر |
| `ALTCHA_EXPIRE_SECONDS` | `300` | عمر هر challenge (ثانیه) |
### جریان
1. کلاینت `GET /api/v1/altcha/challenge` را می‌گیرد (challenge امضاشده).
2. `<altcha-widget>` در پس‌زمینه PoW را حل می‌کند.
3. مقدار حل‌شده (base64) با کلید `altcha` در بدنه‌ی درخواستِ endpoint عمومی ارسال می‌شود.
4. سرور با `CaptchaGuard::assertValid($request)` اعتبارسنجی می‌کند (امضا + انقضا + یک‌بارمصرف بودن).
### endpointهای محافظت‌شده
`send-code`، `register`، `otp-login`، `reset-password`، `pre-registration`.
> `rate`/`comment` پشت JWT هستند و کپچا نمی‌گیرند (بی‌فایده است).
### افزودن کپچا به endpoint عمومی جدید
`CaptchaGuard` را inject کن و اولین خط handler:
```php
$this->captcha->assertValid($request); // پرتاب ERR_CAPTCHA_001 (422) در صورت شکست
```
سپس مسیر را در فرانت به بدنه‌ی درخواست `altcha` وصل کن.
### تغییر سختی
`ALTCHA_MAX_NUMBER` را بالا/پایین ببر (مثلاً `1000000` برای سخت‌تر).
### غیرفعال‌سازی
`ALTCHA_ENABLED=false` → guard کاملاً no-op می‌شود (dev/test همیشه این‌طور است).
### Troubleshooting
- **همیشه ERR_CAPTCHA_001:** کلید `ALTCHA_HMAC_KEY` بین challenge و verify باید یکسان باشد؛ اگر بعد از صدور challenge کلید عوض شود، امضا نامعتبر می‌شود.
- **challenge منقضی:** ساعت سرور را چک کن؛ `ALTCHA_EXPIRE_SECONDS` خیلی کوتاه نباشد.
- **replay:** هر challenge یک‌بار مصرف است؛ برای هر submit یک challenge تازه بگیر.
- **خطای Redis:** pool اختصاصی `altcha.pool` روی `REDIS_URL` است؛ در دسترس بودن Redis لازم است.
+43
View File
@@ -0,0 +1,43 @@
import React, { useEffect, useRef } from 'react';
import 'altcha';
// ALTCHA خودمیزبان: widget با گرفتن challenge از بک‌اند، proof-of-work را در
// پس‌زمینه‌ی مرورگر حل می‌کند و مقدار base64 حل‌شده را در event `verified` می‌دهد.
// این مقدار باید در بدنه‌ی درخواستِ endpoint عمومی با کلید `altcha` ارسال شود.
interface AltchaProps {
onVerified: (payload: string) => void;
challengeUrl?: string;
}
// altcha-widget یک custom element است؛ به JSX معرفی می‌شود (React 19: namespace زیر React.JSX).
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'altcha-widget': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
challengeurl?: string;
};
}
}
}
export default function Altcha({ onVerified, challengeUrl = '/api/v1/altcha/challenge' }: AltchaProps) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onStateChange = (e: Event) => {
const detail = (e as CustomEvent).detail as { state?: string; payload?: string };
if (detail?.state === 'verified' && detail.payload) {
onVerified(detail.payload);
}
};
el.addEventListener('statechange', onStateChange);
return () => el.removeEventListener('statechange', onStateChange);
}, [onVerified]);
return <altcha-widget ref={ref as React.Ref<HTMLElement>} challengeurl={challengeUrl} />;
}
+1
View File
@@ -10,6 +10,7 @@
"ext-ctype": "*",
"ext-iconv": "*",
"ext-soap": "*",
"altcha-org/altcha": "^2.0",
"doctrine/doctrine-bundle": ">=2.18.3",
"doctrine/doctrine-migrations-bundle": "*",
"doctrine/orm": "^3.6",
Generated
+50 -2
View File
@@ -4,8 +4,55 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e8ccf55959566a5f4e99d0965b1c988a",
"content-hash": "9cc89627fd2cff50402b8c5e93a9efee",
"packages": [
{
"name": "altcha-org/altcha",
"version": "v2.0.3",
"source": {
"type": "git",
"url": "https://github.com/altcha-org/altcha-lib-php.git",
"reference": "8b02ad2c9b82998613bac4d093517d457061cbbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/altcha-org/altcha-lib-php/zipball/8b02ad2c9b82998613bac4d093517d457061cbbe",
"reference": "8b02ad2c9b82998613bac4d093517d457061cbbe",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.72",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5 || ^11.5"
},
"type": "library",
"autoload": {
"psr-4": {
"AltchaOrg\\Altcha\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Regeci",
"email": "536331+ovx@users.noreply.github.com"
}
],
"description": "A lightweight PHP library for creating and verifying ALTCHA challenges.",
"support": {
"issues": "https://github.com/altcha-org/altcha-lib-php/issues",
"source": "https://github.com/altcha-org/altcha-lib-php/tree/v2.0.3"
},
"time": "2026-07-02T16:36:58+00:00"
},
{
"name": "doctrine/collections",
"version": "2.6.0",
@@ -9689,7 +9736,8 @@
"platform": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*"
"ext-iconv": "*",
"ext-soap": "*"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
+4
View File
@@ -2,3 +2,7 @@ framework:
cache:
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_URL)%'
pools:
altcha.pool:
adapter: cache.adapter.redis
default_lifetime: 600
+1 -1
View File
@@ -33,7 +33,7 @@ security:
provider: api_doc_provider
public_endpoints:
pattern: ^/(api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/[^/]+$|api/v1/specialties|api/v1/blogs$|api/v1/tags$|api/v1/clinic-invitation/|api/v1/pre-registration$)
pattern: ^/(api/v1/altcha/challenge$|api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/[^/]+$|api/v1/specialties|api/v1/blogs$|api/v1/tags$|api/v1/clinic-invitation/|api/v1/pre-registration$)
stateless: true
security: false
+8
View File
@@ -49,6 +49,14 @@ services:
$baseUrl: '%env(default:default_api_ir_base_url:API_IR_BASE_URL)%'
$token: '%env(default::API_IR_TOKEN)%'
App\Shared\Captcha\AltchaService:
arguments:
$hmacKey: '%env(ALTCHA_HMAC_KEY)%'
$enabled: '%env(bool:ALTCHA_ENABLED)%'
$maxNumber: '%env(int:ALTCHA_MAX_NUMBER)%'
$expireSeconds: '%env(int:ALTCHA_EXPIRE_SECONDS)%'
$altchaPool: '@altcha.pool'
# Persist warning+ logs to the app_log table while keeping stderr output.
App\Shared\Logging\DbLogger:
decorates: 'logger'
+4
View File
@@ -3,6 +3,8 @@
> **Prefix:** `/api/v1/user` and `/oauth`
> **Permission:** All endpoints in this module are **PUBLIC** (no JWT required) except `userinfo` and `logout`
> **🛡️ ALTCHA captcha:** وقتی `ALTCHA_ENABLED=true` است (در prod)، endpointهای `send-code`، `register`، `otp-login` و `reset-password` علاوه بر بدنه‌ی خود، فیلد `altcha` (payload حل‌شده‌ی widget) را الزامی می‌کنند؛ در غیر این صورت `422` با کد `ERR_CAPTCHA_001` برمی‌گردد. جزئیات و مسیر challenge در [captcha.md](captcha.md).
---
## POST `/api/v1/user/send-code`
@@ -548,6 +550,8 @@ Submit a pre-registration request (doctor or clinic). Public endpoint — no aut
**Permission:** Public
> **🛡️ ALTCHA:** وقتی `ALTCHA_ENABLED=true` است، فیلد `altcha` (payload حل‌شده‌ی widget) الزامی است؛ در غیر این صورت `422` با `ERR_CAPTCHA_001`. رجوع به [captcha.md](captcha.md).
### Request Body
```json
{
+69
View File
@@ -0,0 +1,69 @@
# Captcha API (ALTCHA)
کپچای خودمیزبان مبتنی بر ALTCHA (Proof-of-Work، بدون تصویر، بدون سرویس خارجی). برای محافظت از endpointهای عمومی و بدون احراز هویت در برابر اسپم/بات.
- فعال/غیرفعال با `ALTCHA_ENABLED` (پیش‌فرض `false` در dev/test، در prod باید `true` شود).
- وقتی غیرفعال است، همه‌ی endpointها بدون فیلد `altcha` کار می‌کنند.
---
## GET `/api/v1/altcha/challenge`
صدور یک challenge امضاشده برای حل proof-of-work سمت مرورگر. **عمومی** (بدون توکن).
### Response `200`
```json
{
"algorithm": "SHA-256",
"challenge": "417af7b4d4b661c437b99c7e4d5d55747d36f01a8c28d77013af95683b71e642",
"maxnumber": 100000,
"salt": "1d5e0b983af8930ed6d4081f?expires=1783665554&",
"signature": "5d797fdbe9d9cd322fdb02c95c4a673ea92fae09bb63122091713895d0897a11"
}
```
- `signature` — HMAC-SHA256 روی challenge با کلید سرور (`ALTCHA_HMAC_KEY`). قابل جعل نیست.
- `salt` شامل `expires` است؛ پس از انقضا challenge نامعتبر می‌شود.
- خروجی مستقیماً به `<altcha-widget>` داده می‌شود (پاسخ خام است، نه envelope استاندارد).
---
## اعمال کپچا روی endpointهای محافظت‌شده
کلاینت مقدار حل‌شده‌ی widget (base64) را با کلید `altcha` در بدنه‌ی همان درخواست می‌فرستد:
```json
{ "mobile": "09120000000", "altcha": "eyJhbGdvcml0aG0iOiJTSEEt..." }
```
endpointهایی که وقتی `ALTCHA_ENABLED=true` است فیلد `altcha` را الزامی می‌کنند:
| Endpoint | Method |
|---|---|
| `/api/v1/user/send-code` | POST |
| `/api/v1/user/register` | POST |
| `/api/v1/user/otp-login` | POST |
| `/api/v1/user/reset-password` | POST |
| `/api/v1/pre-registration` | POST |
> endpointهای امتیاز/نظر (`POST /api/v1/rate`، `POST /api/v1/comment`) پشت JWT هستند (کاربر لاگین‌شده)، بنابراین کپچا نمی‌گیرند — بات برای رسیدن به آن‌ها باید توکن معتبر داشته باشد که خودش از مسیر OTP (کپچا‌دار) عبور می‌کند.
### خطای اعتبارسنجی کپچا `422`
```json
{
"success": false,
"data": null,
"errors": [{ "code": "ERR_CAPTCHA_001", "field": "altcha", "message": "تأیید امنیتی ناموفق بود. لطفاً صفحه را رفرش کنید و دوباره تلاش کنید" }]
}
```
این خطا زمانی برمی‌گردد که `altcha` غایب، نامعتبر، منقضی، یا **قبلاً استفاده شده** (replay) باشد. هر challenge فقط **یک‌بار** معتبر است (امضایش در Redis سوزانده می‌شود).
---
## امنیت
- Challenge با HMAC-SHA256 و کلید سرور امضا می‌شود؛ کلید هرگز به client نمی‌رود.
- انقضا (`ALTCHA_EXPIRE_SECONDS`) داخل salt امضاشده است.
- استفاده‌ی یک‌باره: signature هر challenge در pool اختصاصی Redis (`altcha.pool`) تا زمان انقضا نگه‌داری می‌شود → ضدِ replay.
- کپچا مکملِ rate-limit موجود است، نه جایگزین آن.
+12 -13
View File
@@ -632,12 +632,10 @@
"630": "Community 630",
"631": "Community 631",
"632": "Community 632",
"633": "Community 633",
"634": "Community 634",
"635": "Community 635",
"636": "Community 636",
"637": "Community 637",
"638": "Community 638",
"639": "Community 639",
"640": "Community 640",
"641": "Community 641",
@@ -666,7 +664,6 @@
"664": "Community 664",
"665": "Community 665",
"666": "Community 666",
"667": "Community 667",
"668": "Community 668",
"669": "Community 669",
"670": "Community 670",
@@ -676,7 +673,6 @@
"674": "Community 674",
"675": "Community 675",
"676": "Community 676",
"677": "Community 677",
"678": "Community 678",
"679": "Community 679",
"680": "Community 680",
@@ -688,11 +684,9 @@
"686": "Community 686",
"687": "Community 687",
"688": "Community 688",
"689": "Community 689",
"690": "Community 690",
"691": "Community 691",
"692": "Community 692",
"693": "Community 693",
"694": "Community 694",
"695": "Community 695",
"696": "Community 696",
"697": "Community 697",
@@ -702,23 +696,28 @@
"702": "Community 702",
"703": "Community 703",
"704": "Community 704",
"705": "Community 705",
"706": "Community 706",
"707": "Community 707",
"708": "Community 708",
"709": "Community 709",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"720": "Community 720",
"721": "Community 721",
"722": "Community 722",
"725": "Community 725",
"723": "Community 723",
"726": "Community 726",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"732": "Community 732",
"734": "Community 734",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738"
}
+159 -164
View File
@@ -1,16 +1,16 @@
# Graph Report - clinicpro (2026-07-10)
## Corpus Check
- 728 files · ~538,154 words
- 736 files · ~541,238 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 9204 nodes · 12739 edges · 722 communities (577 shown, 145 thin omitted)
- 9268 nodes · 12820 edges · 721 communities (576 shown, 145 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 279 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `9d2023a7`
- Built from commit: `11efed41`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -636,12 +636,10 @@
- [[_COMMUNITY_Community 625|Community 625]]
- [[_COMMUNITY_Community 631|Community 631]]
- [[_COMMUNITY_Community 632|Community 632]]
- [[_COMMUNITY_Community 633|Community 633]]
- [[_COMMUNITY_Community 634|Community 634]]
- [[_COMMUNITY_Community 635|Community 635]]
- [[_COMMUNITY_Community 636|Community 636]]
- [[_COMMUNITY_Community 637|Community 637]]
- [[_COMMUNITY_Community 638|Community 638]]
- [[_COMMUNITY_Community 639|Community 639]]
- [[_COMMUNITY_Community 640|Community 640]]
- [[_COMMUNITY_Community 641|Community 641]]
@@ -668,7 +666,6 @@
- [[_COMMUNITY_Community 664|Community 664]]
- [[_COMMUNITY_Community 665|Community 665]]
- [[_COMMUNITY_Community 666|Community 666]]
- [[_COMMUNITY_Community 667|Community 667]]
- [[_COMMUNITY_Community 668|Community 668]]
- [[_COMMUNITY_Community 669|Community 669]]
- [[_COMMUNITY_Community 671|Community 671]]
@@ -677,7 +674,6 @@
- [[_COMMUNITY_Community 674|Community 674]]
- [[_COMMUNITY_Community 675|Community 675]]
- [[_COMMUNITY_Community 676|Community 676]]
- [[_COMMUNITY_Community 677|Community 677]]
- [[_COMMUNITY_Community 678|Community 678]]
- [[_COMMUNITY_Community 679|Community 679]]
- [[_COMMUNITY_Community 680|Community 680]]
@@ -689,11 +685,9 @@
- [[_COMMUNITY_Community 686|Community 686]]
- [[_COMMUNITY_Community 687|Community 687]]
- [[_COMMUNITY_Community 688|Community 688]]
- [[_COMMUNITY_Community 689|Community 689]]
- [[_COMMUNITY_Community 690|Community 690]]
- [[_COMMUNITY_Community 691|Community 691]]
- [[_COMMUNITY_Community 692|Community 692]]
- [[_COMMUNITY_Community 693|Community 693]]
- [[_COMMUNITY_Community 694|Community 694]]
- [[_COMMUNITY_Community 695|Community 695]]
- [[_COMMUNITY_Community 696|Community 696]]
- [[_COMMUNITY_Community 697|Community 697]]
@@ -703,29 +697,34 @@
- [[_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 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 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 725|Community 725]]
- [[_COMMUNITY_Community 723|Community 723]]
- [[_COMMUNITY_Community 726|Community 726]]
- [[_COMMUNITY_Community 727|Community 727]]
- [[_COMMUNITY_Community 728|Community 728]]
- [[_COMMUNITY_Community 729|Community 729]]
- [[_COMMUNITY_Community 730|Community 730]]
- [[_COMMUNITY_Community 731|Community 731]]
- [[_COMMUNITY_Community 732|Community 732]]
- [[_COMMUNITY_Community 734|Community 734]]
- [[_COMMUNITY_Community 735|Community 735]]
- [[_COMMUNITY_Community 736|Community 736]]
- [[_COMMUNITY_Community 737|Community 737]]
- [[_COMMUNITY_Community 738|Community 738]]
## God Nodes (most connected - your core abstractions)
1. `BaseController` - 78 edges
2. `ApiTestCase` - 74 edges
1. `BaseController` - 80 edges
2. `ApiTestCase` - 76 edges
3. `api` - 55 edges
4. `UserProfile` - 52 edges
5. `Clinic` - 50 edges
@@ -738,23 +737,23 @@
## Surprising Connections (you probably didn't know these)
- `ServiceTariffModal()` --calls--> `formatNumber()` [EXTRACTED]
assets/admin/components/ServiceTariffModal.tsx → assets/admin/lib/utils.ts
- `SmsPage()` --calls--> `formatDateTime()` [EXTRACTED]
assets/admin/pages/SmsPage.tsx → assets/admin/lib/utils.ts
- `NewAppointmentModal()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/AppointmentsPage.tsx → assets/admin/stores/authStore.ts
- `LogoUploadField()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/CategoriesPage.tsx → assets/admin/stores/authStore.ts
- `TabActions()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/CategoriesPage.tsx → assets/admin/stores/authStore.ts
- `DashboardPage()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/DashboardPage.tsx → assets/admin/stores/authStore.ts
## Import Cycles
- None detected.
## Communities (722 total, 145 thin omitted)
## Communities (721 total, 145 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (48): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, get, BeforeInstallPromptEvent (+40 more)
Nodes (43): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, AddForm, addSchema (+35 more)
### Community 1 - "Community 1"
Cohesion: 0.03
@@ -769,8 +768,8 @@ 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.08
Nodes (5): Doctor, Collection, self, User, WeeklySchedule
Cohesion: 0.06
Nodes (8): DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule, ManagerRegistry
### Community 5 - "Community 5"
Cohesion: 0.07
@@ -785,8 +784,8 @@ Cohesion: 0.07
Nodes (5): Clinic, Collection, Doctor, self, User
### Community 8 - "Community 8"
Cohesion: 0.07
Nodes (27): Contract, InsuranceOption, KIND_LABEL, AdminLayout(), Topbar(), DEGREE_OPTIONS, DoctorFormPage(), FormValues (+19 more)
Cohesion: 0.08
Nodes (27): useSubscription(), AdminLayout(), avatarBg(), buildSections(), HUES, Props, ROLE_LABELS, Section (+19 more)
### Community 9 - "Community 9"
Cohesion: 0.04
@@ -805,8 +804,8 @@ Cohesion: 0.05
Nodes (44): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+36 more)
### Community 13 - "Community 13"
Cohesion: 0.08
Nodes (21): api, ApiError, downloadFile(), getToken(), refreshOnce(), request(), { refreshMock, logoutMock }, replaceMock (+13 more)
Cohesion: 0.06
Nodes (29): get, PaymentConfig, PaymentGatewayInfo, api, ApiError, downloadFile(), getToken(), refreshOnce() (+21 more)
### Community 14 - "Community 14"
Cohesion: 0.10
@@ -817,12 +816,12 @@ 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.06
Nodes (8): PatientSession, SmsWallet, AppLog, Appointment, Collection, PatientRecord, self, SessionService
### Community 17 - "Community 17"
Cohesion: 0.05
Nodes (38): DELETE `/api/v1/comment/{uuid}`, Errors, Errors, Errors, Errors, Errors, Errors, Errors (+30 more)
Cohesion: 0.06
Nodes (33): DELETE `/api/v1/comment/{uuid}`, Errors, Errors, Errors, Errors, Errors, Errors, Errors (+25 more)
### Community 18 - "Community 18"
Cohesion: 0.05
@@ -830,7 +829,7 @@ Nodes (38): API, API, API, API, API, Route, Route, Route (+30 more)
### Community 19 - "Community 19"
Cohesion: 0.03
Nodes (59): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm, provinceSchema (+51 more)
Nodes (88): ApiResponse, PaginatedResponse, STATUS_FILTERS, CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema (+80 more)
### Community 20 - "Community 20"
Cohesion: 0.13
@@ -838,11 +837,11 @@ Nodes (4): AdminApiController, JsonResponse, Request, StreamedResponse
### Community 21 - "Community 21"
Cohesion: 0.05
Nodes (34): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+26 more)
Nodes (33): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+25 more)
### Community 22 - "Community 22"
Cohesion: 0.09
Nodes (15): RatingController, Like, Rate, CommentListNPlusOneTest, LikeRepository, RateRepository, JsonResponse, Request (+7 more)
Cohesion: 0.15
Nodes (9): RatingController, Like, LikeRepository, JsonResponse, Request, User, Comment, ManagerRegistry (+1 more)
### Community 23 - "Community 23"
Cohesion: 0.05
@@ -872,6 +871,10 @@ 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.09
Nodes (13): BillingCalculatorTest, BillingCalculator, Money, BillingCalculator, InvoiceService, PatientService, CoverageRule, ShareBreakdown (+5 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)
@@ -886,7 +889,7 @@ Nodes (21): CoverageRow, Draft, KIND, TenantInsurance, ServiceTariffModal(), Tar
### Community 34 - "Community 34"
Cohesion: 0.06
Nodes (35): require, doctrine/doctrine-bundle, doctrine/doctrine-migrations-bundle, doctrine/orm, ext-ctype, ext-iconv, ext-soap, lexik/jwt-authentication-bundle (+27 more)
Nodes (36): require, altcha-org/altcha, doctrine/doctrine-bundle, doctrine/doctrine-migrations-bundle, doctrine/orm, ext-ctype, ext-iconv, ext-soap (+28 more)
### Community 35 - "Community 35"
Cohesion: 0.06
@@ -901,7 +904,7 @@ Cohesion: 0.06
Nodes (31): API endpoint موجود برای آدرس دکتر:, `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}` — حذف, `docs/api/appointment-settings.md`:, `docs/api/clinic.md`:, Endpoint موجود (workaround که حذف می‌شود):, Entity `DoctorAddress`:, Frontend موجود (`DoctorDetailPage.tsx`):, `GET /api/v1/clinic/{clinicUuid}/addresses` — لیست آدرس‌ها (+23 more)
### Community 38 - "Community 38"
Cohesion: 0.22
Cohesion: 0.25
Nodes (8): Authentication API, DELETE `/api/v1/notification-mobile/{target}`, Errors, Notification Mobile (OTP), POST `/oauth/logout`, Request Body, Response `200`, Response `200`
### Community 39 - "Community 39"
@@ -913,8 +916,8 @@ Cohesion: 0.09
Nodes (4): User, PasswordAuthenticatedUserInterface, self, UserInterface
### Community 41 - "Community 41"
Cohesion: 0.07
Nodes (29): ApiResponse, PaginatedResponse, STATUS_FILTERS, FILTERS, Breakdown, SOURCE_LABEL, Summary, LEVEL_FILTER_OPTIONS (+21 more)
Cohesion: 0.11
Nodes (17): endpointهای عمومی که باید محافظت شوند, زمینه, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه, یکپارچه‌سازی ALTCHA (کپچای Self-Hosted، Proof-of-Work) روی endpointهای عمومی (+9 more)
### Community 42 - "Community 42"
Cohesion: 0.07
@@ -945,8 +948,8 @@ Cohesion: 0.11
Nodes (4): Payment, Appointment, self, User
### Community 49 - "Community 49"
Cohesion: 0.07
Nodes (22): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, NewAppointmentModal() (+14 more)
Cohesion: 0.08
Nodes (19): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, NewAppointmentModal() (+11 more)
### Community 50 - "Community 50"
Cohesion: 0.07
@@ -961,8 +964,8 @@ Cohesion: 0.07
Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, باگ ۱ — کرش تقویم, باگ ۲ — روز هفته در DateNavigator, باگ ۳ — پیام «slot نیست», باگ ۴ — نوبت جدید: نام اجباری + find-or-create patient, باگ ۵ — patient_mobile نشان می‌دهد موبایل پزشک, باگ ۶ — نوبت‌های رزرو شده در نمایش زمانبندی (+18 more)
### Community 53 - "Community 53"
Cohesion: 0.08
Nodes (15): AppLogRepository, ClaimItemRepository, InvoiceItemRepository, PreRegistrationRepository, SessionServiceRepository, SiteConfigRepository, ServiceEntityRepository, ManagerRegistry (+7 more)
Cohesion: 0.10
Nodes (13): AppLogRepository, ClaimItemRepository, PreRegistrationRepository, ProvinceRepository, SessionServiceRepository, ServiceEntityRepository, ManagerRegistry, ManagerRegistry (+5 more)
### Community 54 - "Community 54"
Cohesion: 0.10
@@ -1001,12 +1004,12 @@ 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.05
Nodes (45): FreeVisitPrice(), Pricing, cn(), formatDateTime(), formatRial(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile() (+37 more)
Cohesion: 0.06
Nodes (42): FreeVisitPrice(), Pricing, cn(), formatRial(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile() (+34 more)
### Community 64 - "Community 64"
Cohesion: 0.19
Nodes (7): SmsWalletController, SmsSettingsRepository, SmsSettings, JsonResponse, Request, User, ManagerRegistry
Cohesion: 0.29
Nodes (4): SmsWalletController, JsonResponse, Request, User
### Community 65 - "Community 65"
Cohesion: 0.08
@@ -1029,8 +1032,8 @@ Cohesion: 0.15
Nodes (12): رفع بهم‌ریختگی کامل پنل ادمین روی iPhone 8 (Safari/Chrome iOS), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more)
### Community 71 - "Community 71"
Cohesion: 0.09
Nodes (19): AddForm, addSchema, ClinicsPage(), HUES_LIST, EMPTY, PreRegistration, STATUS_META, STATUS_TABS (+11 more)
Cohesion: 0.08
Nodes (22): Contract, InsuranceOption, KIND_LABEL, ChargeForm, chargeSchema, EMPTY_LOGS, POST_VISIT_VARS, REMINDER_HOUR_OPTIONS (+14 more)
### Community 72 - "Community 72"
Cohesion: 0.09
@@ -1081,8 +1084,8 @@ Cohesion: 0.07
Nodes (30): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, core-js, @csstools/postcss-oklab-function, @hotwired/stimulus (+22 more)
### Community 86 - "Community 86"
Cohesion: 0.06
Nodes (15): AppointmentExpiryServiceTest, DateOverrideOwnershipTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser (+7 more)
Cohesion: 0.05
Nodes (16): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser (+8 more)
### Community 87 - "Community 87"
Cohesion: 0.10
@@ -1105,7 +1108,7 @@ Cohesion: 0.10
Nodes (20): DELETE `/api/v1/admin/users/{uuid}`, Errors, Errors, GET `/api/v1/admin/users`, GET `/api/v1/admin/users/stats`, GET `/api/v1/admin/users/{uuid}`, POST `/api/v1/admin/users/{uuid}/status`, PUT `/api/v1/admin/users/{uuid}` (+12 more)
### Community 92 - "Community 92"
Cohesion: 0.09
Cohesion: 0.10
Nodes (21): Bulk import / export, DELETE `/api/v1/admin/doctor-service/{id}`, Doctor Service API, Errors, Errors, Errors, Errors, GET `/api/v1/admin/doctor-services` (+13 more)
### Community 93 - "Community 93"
@@ -1169,8 +1172,8 @@ Cohesion: 0.33
Nodes (6): AppointmentController, Appointment, Doctor, JsonResponse, Request, User
### Community 108 - "Community 108"
Cohesion: 0.13
Nodes (10): BaseController, CategoryController, CategoryImportController, SiteContextController, JsonResponse, JsonResponse, Request, JsonResponse (+2 more)
Cohesion: 0.11
Nodes (12): CaptchaController, BaseController, CategoryController, CategoryImportController, SiteContextController, JsonResponse, JsonResponse, Request (+4 more)
### Community 109 - "Community 109"
Cohesion: 0.29
@@ -1377,8 +1380,8 @@ Cohesion: 0.13
Nodes (13): Architecture, Auth, Backend (PHP/Symfony), Backend — `src/`, Category / Bundle system, Commands, Database, First-time setup (+5 more)
### Community 163 - "Community 163"
Cohesion: 0.13
Nodes (14): autoload, autoload-dev, psr-4, psr-4, conflict, symfony/symfony, description, license (+6 more)
Cohesion: 0.11
Nodes (18): autoload, autoload-dev, psr-4, psr-4, conflict, symfony/symfony, description, extra (+10 more)
### Community 164 - "Community 164"
Cohesion: 0.29
@@ -1401,8 +1404,8 @@ Cohesion: 0.10
Nodes (20): api.ir (استعلام هویت — Shahkar / IbanMatch), اتصال به دیتابیس‌های مستقل (الزامی), اسرار (الزامی — قبل از اولین دیپلوی), امنیت و منابع, بررسی سلامت, دامنه‌ها و CORS, دیپلوی‌های بعدی, راهنمای دیپلوی ClinicPro (Coolify + Docker Compose) (+12 more)
### Community 169 - "Community 169"
Cohesion: 0.22
Nodes (4): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, ManagerRegistry
Cohesion: 0.15
Nodes (5): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, TenantInsuranceCleanupService, ManagerRegistry
### Community 170 - "Community 170"
Cohesion: 0.13
@@ -1452,10 +1455,6 @@ Nodes (3): PatientRecord, Collection, User
Cohesion: 0.23
Nodes (3): WeeklySchedule, Doctor, self
### Community 182 - "Community 182"
Cohesion: 0.07
Nodes (5): SmsSettings, SmsWallet, LogPruneService, AppointmentExpiryService, self
### Community 183 - "Community 183"
Cohesion: 0.14
Nodes (13): `UserDetailPage.tsx` — فقط user query, زمینه, فایل‌های مرتبط, قرارداد پروفایل (`UserProfile.toArray`), مشکل / هدف, نمایش پروفایل کاربر در صفحه‌ی جزئیات کاربرِ پنل ادمین, نکات مهم, وضعیت فعلی (کد واقعی) (+5 more)
@@ -1526,7 +1525,7 @@ Nodes (13): Endpoint ها, PATCH /api/v1/secretary/{uuid}, POST /api/v1/secretar
### Community 201 - "Community 201"
Cohesion: 0.12
Nodes (16): Admin API, Clinic Invitation Management, Dashboard, GET `/api/v1/admin/dashboard/charts`, GET `/api/v1/admin/dashboard/recent`, GET `/api/v1/admin/dashboard/stats`, GET `/api/v1/admin/pre-registrations`, GET /api/v1/admin/settings (+8 more)
Nodes (16): Admin API, Clinic Invitation Management, Dashboard, GET `/api/v1/admin/dashboard/charts`, GET `/api/v1/admin/dashboard/recent`, GET `/api/v1/admin/dashboard/stats`, GET `/api/v1/admin/representations`, GET /api/v1/admin/settings (+8 more)
### Community 202 - "Community 202"
Cohesion: 0.15
@@ -1545,8 +1544,8 @@ Cohesion: 0.07
Nodes (20): Command, CancelExpiredAppointmentsCommand, CreateAdminCommand, PruneLogsCommand, SeedCategoriesCommand, SeedDemoDataCommand, SeedSmsMessageTemplatesCommand, InputInterface (+12 more)
### Community 206 - "Community 206"
Cohesion: 0.08
Nodes (14): MellatGateway, MockGateway, SepGateway, SoapClient, PaymentGatewayInterface, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult (+6 more)
Cohesion: 0.06
Nodes (17): GatewayFactory, MellatGateway, MockGateway, SepGateway, MellatGateway, SepGateway, SoapClient, PaymentGatewayInterface (+9 more)
### Community 207 - "Community 207"
Cohesion: 0.15
@@ -1605,8 +1604,8 @@ Cohesion: 0.23
Nodes (5): PaymentRepository, Appointment, ManagerRegistry, Payment, User
### Community 225 - "Community 225"
Cohesion: 0.09
Nodes (18): formatDate(), toDate(), ALL_STATUSES, AppointmentDetailPage(), timeOf(), Claim, ClaimItem, DebtRow (+10 more)
Cohesion: 0.05
Nodes (36): formatDate(), formatDateTime(), toDate(), ALL_STATUSES, AppointmentDetailPage(), timeOf(), Claim, ClaimItem (+28 more)
### Community 226 - "Community 226"
Cohesion: 0.15
@@ -1617,8 +1616,8 @@ Cohesion: 0.17
Nodes (11): تشخیص عمیق down شدن سرور بعد از ~۱۰ سیکل + وریفای و تکمیل فیکس‌های پایداری, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, ۱. وریفای فیکس‌های repo (idempotent) (+3 more)
### Community 228 - "Community 228"
Cohesion: 0.04
Nodes (47): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/billing/tenant-insurances/{uuid}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, Errors, Errors, Errors (+39 more)
Cohesion: 0.20
Nodes (10): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, GET `/api/v1/insurance/{id}`, Insurance API, Response `200`, Response `200` (+2 more)
### Community 229 - "Community 229"
Cohesion: 0.14
@@ -1793,12 +1792,12 @@ Cohesion: 0.18
Nodes (10): Endpoint های موجود که تغییر می‌کنند, GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX, GET /api/v1/dashboard/clinic, GET /api/v1/dashboard/doctor, تسک ۱۶: داشبورد هوشمند — چارت + فیلتر زمانی, توضیح, زمان تخمینی, فیلتر بازه زمانی (+2 more)
### Community 274 - "Community 274"
Cohesion: 0.20
Cohesion: 0.15
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
### Community 275 - "Community 275"
Cohesion: 0.24
Nodes (4): MellatGateway, MellatGatewayTest, ErrorCodesTest, TestCase
Cohesion: 0.19
Nodes (5): MellatGatewayTest, ErrorCodesTest, KavehNegarProviderTest, TestCase, KavehNegarProvider
### Community 276 - "Community 276"
Cohesion: 0.20
@@ -1897,8 +1896,8 @@ Cohesion: 0.42
Nodes (3): PreRegistrationController, JsonResponse, Request
### Community 301 - "Community 301"
Cohesion: 0.07
Nodes (18): ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP, IRAN_CENTER (+10 more)
Cohesion: 0.06
Nodes (22): ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP, IRAN_CENTER (+14 more)
### Community 302 - "Community 302"
Cohesion: 0.12
@@ -1910,7 +1909,7 @@ Nodes (16): آماده‌سازی پروژه ClinicPro برای دیپلوی ر
### Community 304 - "Community 304"
Cohesion: 0.04
Nodes (47): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 36. 🟢 `GET` get my rate, 37. 🔵 `POST` post, 38. 🟢 `GET` Unapproved comments, 39. 🟡 `PATCH` Comment confirmation (+39 more)
Nodes (48): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 35. 🟡 `PATCH` patch, 36. 🟢 `GET` get my rate, 37. 🔵 `POST` post, 38. 🟢 `GET` Unapproved comments (+40 more)
### Community 306 - "Community 306"
Cohesion: 0.07
@@ -2009,8 +2008,8 @@ Cohesion: 0.22
Nodes (8): Query های جدید, بیماران منحصربه‌فرد در بازه, درآمد بر اساس روز (از patient_sessions), فروش اشتراک بر اساس پنل (admin), نوبت‌ها بر اساس روز (admin chart), نکات مهم, هیچ migration لازم نیست, پایگاه داده — تسک ۱۶: داشبورد هوشمند
### Community 333 - "Community 333"
Cohesion: 0.25
Nodes (7): DELETE `/api/v1/clinic-pro/doctor-address/{id}`, Doctor API, Errors, GET `/api/v1/doctors`, Query Parameters, Response `200`, Response `200`
Cohesion: 0.05
Nodes (41): DELETE `/api/v1/clinic-pro/doctor-address/{id}`, DELETE `/api/v1/doctor/{uuid}`, Doctor API, Errors, Errors, Errors, Errors, Errors (+33 more)
### Community 334 - "Community 334"
Cohesion: 0.25
@@ -2065,8 +2064,8 @@ Cohesion: 0.11
Nodes (18): زمینه, فاز ۰ — Baseline, فاز ۱ — قرارداد پاسخ و Envelope, فاز ۲ — احراز هویت و مجوزها (Auth / RBAC), فاز ۳ — اعتبارسنجی ورودی و مدیریت خطا, فاز ۴ — امنیت API, فاز ۵ — پنل ادمین React SPA, فاز ۶ — یکپارچگی و سناریوهای واقعی کاربر (E2E) (+10 more)
### Community 348 - "Community 348"
Cohesion: 0.39
Nodes (3): ProvinceRepository, ManagerRegistry, Province
Cohesion: 0.43
Nodes (3): SmsTemplateRepository, SmsTemplate, ManagerRegistry
### Community 349 - "Community 349"
Cohesion: 0.18
@@ -2137,8 +2136,8 @@ Cohesion: 0.25
Nodes (7): ادمین, دکتر نمونه کامل — تبریز, دکتران bulk (۱۵۰۰ دکتر در ۱۵ شهر), ساخت مجدد, منشی دکتر نمونه, کاربران تستی, کلینیک نمونه — تبریز
### Community 367 - "Community 367"
Cohesion: 0.13
Nodes (12): AdminUserDetail, AVATAR_COLORS, EditForm, editSchema, GENDER_LABELS, getPrimaryRole(), MARITAL_LABELS, MEDICAL_SECTIONS (+4 more)
Cohesion: 0.18
Nodes (5): Altcha, AltchaService, altcha, AltchaProps, IntrinsicElements
### Community 368 - "Community 368"
Cohesion: 0.36
@@ -2258,7 +2257,7 @@ Nodes (7): initiate(), refund(), reverse(), verify(), PaymentInitResult, Payment
### Community 399 - "Community 399"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260611075829
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260628165710
### Community 401 - "Community 401"
Cohesion: 0.12
@@ -2269,7 +2268,7 @@ Cohesion: 0.11
Nodes (17): بازطراحی معماری پرداخت — سرویس‌محور، امن، توسعه‌پذیر (Backend), تست دستی (ddev، در حالت `payment_test_mode=1`), خروجی نهایی (طبق spec — در گزارش اجرا ارائه شود), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی (+9 more)
### Community 407 - "Community 407"
Cohesion: 0.15
Cohesion: 0.16
Nodes (11): MessageBusInterface, MockObject, SmsService, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsService, SmsTextResolver (+3 more)
### Community 418 - "Community 418"
@@ -2408,10 +2407,6 @@ Nodes (3): CommissionService, Payment, Representation
Cohesion: 0.29
Nodes (6): Appointment Settings API, Available Locations, Errors, `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`, Response `200`, Slot Calculation Logic (Reference)
### Community 485 - "Community 485"
Cohesion: 0.43
Nodes (3): SmsMessageController, JsonResponse, Request
### Community 486 - "Community 486"
Cohesion: 0.40
Nodes (5): ۲.۸ تنظیمات نوبت (Appointment Settings), ۲.۸.۱ برنامه هفتگی (Weekly Schedule), ۲.۸.۲ تعطیلات (Holidays), ۲.۸.۳ لغو تعطیل (Date Override), ۲.۸.۴ الگوریتم محاسبه اسلات‌های خالی
@@ -2425,8 +2420,8 @@ 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.06
Nodes (29): PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema (+21 more)
Cohesion: 0.10
Nodes (18): usePaymentConfig(), emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema, PLAN_DISPLAY, PlanForm (+10 more)
### Community 491 - "Community 491"
Cohesion: 0.12
@@ -2665,16 +2660,16 @@ Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/admin/sms/template/{uuid}/reject`, Request Body, Response `200`
### Community 559 - "Community 559"
Cohesion: 0.25
Nodes (4): SmsMessageTemplateRepository, SmsTextResolver, SmsMessageTemplate, ManagerRegistry
Cohesion: 0.16
Nodes (7): SmsMessageController, SmsMessageTemplateRepository, SmsTextResolver, SmsMessageTemplate, JsonResponse, Request, ManagerRegistry
### Community 560 - "Community 560"
Cohesion: 0.50
Nodes (4): GET /api/v1/sms/wallet/balance, GET /api/v1/sms/wallet/logs, POST /api/v1/sms/wallet/charge, SMS Wallet
### Community 561 - "Community 561"
Cohesion: 0.43
Nodes (3): BaseKernel, MicroKernelTrait, Kernel
Cohesion: 0.19
Nodes (6): BaseKernel, Closure, CorsRegexEnvProcessor, EnvVarProcessorInterface, MicroKernelTrait, Kernel
### Community 563 - "Community 563"
Cohesion: 0.67
@@ -2709,8 +2704,8 @@ Cohesion: 0.12
Nodes (15): دیپلوی ClinicPro (Symfony) روی لیارا با Docker, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی (کد واقعی), وظایف, پروژه (+7 more)
### Community 577 - "Community 577"
Cohesion: 0.39
Nodes (3): DoctorService, DoctorServiceRepository, ManagerRegistry
Cohesion: 0.38
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
### Community 581 - "Community 581"
Cohesion: 0.50
@@ -2749,21 +2744,21 @@ Cohesion: 0.50
Nodes (3): Entity: Payment, ساختار فایل‌ها, معماری — تسک ۱۵: ماژول پرداخت
### Community 595 - "Community 595"
Cohesion: 0.40
Nodes (5): Errors, GET `/api/v1/clinic/my-doctor/{doctorUuid}`, Path Parameters, Response `200`, Schedule Fields Notes
Cohesion: 0.43
Nodes (3): SmsLogRepository, SmsLog, ManagerRegistry
### Community 596 - "Community 596"
Cohesion: 0.15
Nodes (3): LoggerInterface, RanginehProvider, ApiIrService
### Community 597 - "Community 597"
Cohesion: 0.29
Nodes (7): Configuration, Errors, GET `/api/v1/admin/sms/templates`, GET `/api/v1/sms/template/{uuid}`, Response `200`, Response `200`, SMS API
Cohesion: 0.20
Nodes (10): Configuration, Errors, GET `/api/v1/admin/sms/templates`, GET `/api/v1/sms/template/{uuid}`, POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`application/json`), Response `200`, Response `200` (+2 more)
### Community 599 - "Community 599"
Cohesion: 0.67
Nodes (3): Errors, POST `/api/v1/sms/template/{uuid}/submit`, Response `200`
### Community 600 - "Community 600"
Cohesion: 0.67
Nodes (3): POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`application/json`), Response `200`
### Community 603 - "Community 603"
Cohesion: 0.67
Nodes (3): Anti-Pattern هایی که مشاهده می‌شوند, Pattern هایی که استفاده شده‌اند, ۳. تحلیل Design Patterns
@@ -2785,16 +2780,20 @@ Cohesion: 0.67
Nodes (3): بک‌اند, فرانت‌اند, وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
### Community 618 - "Community 618"
Cohesion: 0.12
Nodes (6): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantServiceCoverageRepository, TenantInsuranceCleanupService, ManagerRegistry, TenantServiceCoverage
Cohesion: 0.16
Nodes (5): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantServiceCoverageRepository, ManagerRegistry, TenantServiceCoverage
### Community 625 - "Community 625"
Cohesion: 0.40
Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 631 - "Community 631"
Cohesion: 0.50
Nodes (4): GET `/api/v1/admin/representations`, Query Parameters, Representation Management, Response `200`
Nodes (4): 40. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 632 - "Community 632"
Cohesion: 0.50
Nodes (4): DELETE `/api/v1/doctor/{uuid}`, Errors, Path Parameters, Response `200`
Nodes (4): GET `/api/v1/admin/pre-registrations`, POST `/api/v1/admin/pre-registrations/{uuid}/approve`, POST `/api/v1/admin/pre-registrations/{uuid}/reject`, Pre-Registration Management
### Community 634 - "Community 634"
Cohesion: 0.47
@@ -2804,10 +2803,6 @@ Nodes (3): ImageCropModalProps, createImage(), getCroppedImage()
Cohesion: 0.12
Nodes (16): Runbook — تشخیص «ری‌استارت» سرور: recycle عادی یا خرابی واقعی؟, اقدامات تکمیلی روی سرور (خارج از repo), تأیید روی سرور — اسکریپت آماده, جدول تفسیر خروجی اسکریپت, خلاصه یک‌خطی, علامت مشکل, چرا این اتفاق می‌افتاد (و فیکس اعمال‌شده), چک‌لیست رفع (+8 more)
### Community 638 - "Community 638"
Cohesion: 0.40
Nodes (5): Errors, PATCH `/api/v1/doctor/{uuid}`, Path Parameters, Request Body (`application/json`), Response `200`
### Community 640 - "Community 640"
Cohesion: 0.40
Nodes (5): Errors, Notes, POST `/api/v1/notification-mobile/request-otp`, Request Body, Response `200`
@@ -2845,8 +2840,8 @@ Cohesion: 0.38
Nodes (5): MyAppointmentsController, Doctor, JsonResponse, Request, User
### Community 656 - "Community 656"
Cohesion: 0.43
Nodes (3): SmsTemplateRepository, SmsTemplate, ManagerRegistry
Cohesion: 0.33
Nodes (6): Captcha API (ALTCHA), GET `/api/v1/altcha/challenge`, Response `200`, اعمال کپچا روی endpointهای محافظت‌شده, امنیت, خطای اعتبارسنجی کپچا `422`
### Community 657 - "Community 657"
Cohesion: 0.20
@@ -2862,16 +2857,12 @@ Nodes (4): ClinicInvitationService, Clinic, ClinicDoctorInvitation, User
### Community 661 - "Community 661"
Cohesion: 0.43
Nodes (3): SmsLogRepository, SmsLog, ManagerRegistry
Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard()
### Community 662 - "Community 662"
Cohesion: 0.20
Nodes (10): Application Logs, DELETE `/api/v1/admin/logs`, GET `/api/v1/admin/logs`, GET `/api/v1/admin/logs/export`, Log Retention, Query Parameters, Query Parameters, Response `200` (+2 more)
### Community 667 - "Community 667"
Cohesion: 0.40
Nodes (5): Errors, PATCH `/api/v1/clinic-pro/doctor-address/{id}`, Path Parameters, Request Body, Response `200`
### Community 672 - "Community 672"
Cohesion: 0.17
Nodes (11): تشخیص «ری‌استارت» سرور روی Coolify + رفع نویز لاگ + بررسی خطای ACME, زمینه, فایل‌های مرتبط, نکات مهم, وضعیت فعلی, وظایف, پروژه, ۱. کاهش نویز لاگ workerها (بدون تغییر رفتار) (+3 more)
@@ -2881,8 +2872,8 @@ Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/user/register`, Request Body, Response `201`
### Community 674 - "Community 674"
Cohesion: 0.40
Nodes (3): Props, StatTone, TONE
Cohesion: 0.43
Nodes (3): InvoiceItemRepository, InvoiceItem, ManagerRegistry
### Community 675 - "Community 675"
Cohesion: 0.50
@@ -2892,10 +2883,6 @@ Nodes (4): GET `/api/v1/admin/secretaries`, Query Parameters, Response `200`, Se
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/doctor`, Request Body (`application/json`), Response `201`
### Community 677 - "Community 677"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/doctor/{uuid}`, Path Parameters, Response `200`
### Community 678 - "Community 678"
Cohesion: 0.50
Nodes (4): Errors, POST `/oauth/token`, Request Body, Response `200`
@@ -2916,21 +2903,9 @@ 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 689 - "Community 689"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/clinic-pro/doctor-address`, Request Body, Response `201`
### Community 690 - "Community 690"
Cohesion: 0.50
Nodes (4): Errors, Path Parameters, POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`, Response `201`
### Community 692 - "Community 692"
Cohesion: 0.50
Nodes (4): extra, symfony, allow-contrib, require
### Community 693 - "Community 693"
Cohesion: 0.67
Nodes (3): GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`, Path Parameters, Response `200`
### Community 694 - "Community 694"
Cohesion: 0.48
Nodes (3): SmsSettingsRepository, SmsSettings, ManagerRegistry
### Community 695 - "Community 695"
Cohesion: 0.67
@@ -2952,37 +2927,41 @@ Nodes (11): رفع خطای `Class "SoapClient" not found` در پرداخت م
Cohesion: 0.40
Nodes (5): 32. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 705 - "Community 705"
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 (فاز ۱ سیستم صورتحساب)
### Community 706 - "Community 706"
Cohesion: 0.53
Nodes (3): PaymentLog, PaymentLogRepository, ManagerRegistry
### Community 708 - "Community 708"
Cohesion: 0.33
Nodes (4): PatientService, Appointment, PatientRecord, PatientSession
### Community 707 - "Community 707"
Cohesion: 0.40
Nodes (5): Errors, Path Parameters, POST `/api/v1/like/{commentUuid}`, Request Body (`application/json`), Response `200`
### Community 712 - "Community 712"
Cohesion: 0.50
Nodes (4): 44. 🟢 `GET` list comment, هدرهای اضافی, پارامترهای Query, پاسخ‌ها
### Community 714 - "Community 714"
Cohesion: 0.40
Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 718 - "Community 718"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/admin/insurances`, Query Parameters, Response `200`
### Community 715 - "Community 715"
Cohesion: 0.40
Nodes (5): 35. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 719 - "Community 719"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/admin/insurance`, Request Body (`application/json`), Response `201`
### Community 720 - "Community 720"
Cohesion: 0.33
Nodes (3): Closure, CorsRegexEnvProcessor, EnvVarProcessorInterface
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/insurance/`, Request Body (`application/json`), Response `201`
### Community 725 - "Community 725"
Cohesion: 0.43
Nodes (3): InvoiceService, Invoice, PatientSession
### Community 721 - "Community 721"
Cohesion: 0.50
Nodes (4): Errors, PATCH `/api/v1/insurance/{id}`, Request Body, Response `200`
### Community 727 - "Community 727"
Cohesion: 0.53
Nodes (4): Money, BillingCalculator, CoverageRule, ShareBreakdown
### Community 723 - "Community 723"
Cohesion: 0.50
Nodes (4): PUT `/api/v1/insurance-pricing`, Request Body, Response `200`, خطاها
### Community 728 - "Community 728"
Cohesion: 0.50
@@ -2996,28 +2975,44 @@ Nodes (3): Errors, GET `/api/v1/notification-mobile/{target}`, Response `200`
Cohesion: 0.67
Nodes (3): GET `/api/v1/representation/{uuid}/dashboard/yearly`, Query Parameters, Response `200`
### Community 734 - "Community 734"
Cohesion: 0.67
Nodes (3): GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`, PUT `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`, TenantServiceCoverage — پوشش خدمت تحت یک قرارداد بیمه (فاز ۲)
### Community 735 - "Community 735"
Cohesion: 0.67
Nodes (3): GET `/api/v1/insurance-pricing`, Response `200`, خطاها
### Community 736 - "Community 736"
Cohesion: 0.67
Nodes (3): GET `/api/v1/insurances`, Query Parameters, Response `200`
### Community 737 - "Community 737"
Cohesion: 0.67
Nodes (3): POST `/api/v1/admin/insurance/{id}/upload-logo`, Request, Response `200`
### Community 738 - "Community 738"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/representation/{uuid}`, Path Parameters, Response `200`
## Knowledge Gaps
- **4009 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4004 more)
- **4030 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4025 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **145 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 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 433`, `Community 308`, `Community 58`, `Community 59`, `Community 318`, `Community 64`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 485`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 245`, `Community 380`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.026) - this node is a cross-community bridge._
- **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 559`, `Community 433`, `Community 308`, `Community 58`, `Community 59`, `Community 318`, `Community 64`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 245`, `Community 380`, `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 397`, `Community 534`, `Community 535`, `Community 541`, `Community 169`, `Community 562`, `Community 565`, `Community 573`, `Community 574`, `Community 575`, `Community 708`, `Community 717`, `Community 594`, `Community 82`, `Community 609`, `Community 484`, `Community 618`, `Community 497`, `Community 371`?**
_High betweenness centrality (0.020) - this node is a cross-community bridge._
- **Why does `Version20260705070546` connect `Community 646` to `Community 399`?**
_High betweenness centrality (0.019) - this node is a cross-community bridge._
- **Why does `ApiTestCase` connect `Community 86` to `Community 397`, `Community 22`, `Community 535`, `Community 534`, `Community 541`, `Community 169`, `Community 562`, `Community 565`, `Community 573`, `Community 574`, `Community 575`, `Community 594`, `Community 82`, `Community 609`, `Community 484`, `Community 618`, `Community 497`, `Community 371`, `Community 633`?**
_High betweenness centrality (0.018) - this node is a cross-community bridge._
- **What connects `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps` to the rest of the system?**
_4009 weakly-connected nodes found - possible documentation gaps or missing edges._
_4030 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05004389815627744 - nodes in this community are weakly interconnected._
_Cohesion score 0.047107014848950336 - 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?**
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "label": "CaptchaGuard.php", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L1"}, {"id": "captcha_captchaguard_captchaguard", "label": "CaptchaGuard", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L16"}, {"id": "captcha_captchaguard_captchaguard_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L18"}, {"id": "captcha_captchaguard_captchaguard_assertvalid", "label": ".assertValid()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20"}, {"id": "request", "label": "Request", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "errorcodes", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "appexception", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "request", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchaguard_php", "target": "captcha_captchaguard_captchaguard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L16", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard", "target": "captcha_captchaguard_captchaguard_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L18", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard", "target": "captcha_captchaguard_captchaguard_assertvalid", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20", "weight": 1.0}, {"source": "captcha_captchaguard_captchaguard_assertvalid", "target": "request", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaGuard.php", "source_location": "L20", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "enabled", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L22", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "json_decode", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L26", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "getContent", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L26", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "is_array", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L27", "receiver": null}, {"caller_nid": "captcha_captchaguard_captchaguard_assertvalid", "callee": "verifySolution", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaGuard.php", "source_location": "L29", "receiver": null}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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_docs_api_captcha_md", "label": "captcha.md", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_captcha_api_altcha", "label": "Captcha API (ALTCHA)", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_get_api_v1_altcha_challenge", "label": "GET `/api/v1/altcha/challenge`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L10"}, {"id": "api_captcha_response_200", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L14"}, {"id": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "label": "\u0627\u0639\u0645\u0627\u0644 \u06a9\u067e\u0686\u0627 \u0631\u0648\u06cc endpoint\u0647\u0627\u06cc \u0645\u062d\u0627\u0641\u0638\u062a\u200c\u0634\u062f\u0647", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L31"}, {"id": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "label": "\u062e\u0637\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u06a9\u067e\u0686\u0627 `422`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L53"}, {"id": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "label": "\u0627\u0645\u0646\u06cc\u062a", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L66"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "target": "api_captcha_captcha_api_altcha", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L1", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_get_api_v1_altcha_challenge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L10", "weight": 1.0}, {"source": "api_captcha_get_api_v1_altcha_challenge", "target": "api_captcha_response_200", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L14", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L31", "weight": 1.0}, {"source": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "target": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L53", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L66", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
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_src_shared_captcha_captchacontroller_php", "label": "CaptchaController.php", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L1"}, {"id": "captcha_captchacontroller_captchacontroller", "label": "CaptchaController", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L10"}, {"id": "basecontroller", "label": "BaseController", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "captcha_captchacontroller_captchacontroller_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L13"}, {"id": "captcha_captchacontroller_captchacontroller_challenge", "label": ".challenge()", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15"}, {"id": "jsonresponse", "label": "JsonResponse", "file_type": "code", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "basecontroller", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "attributes", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "jsonresponse", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "route", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L8", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_captcha_captchacontroller_php", "target": "captcha_captchacontroller_captchacontroller", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L10", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "basecontroller", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L11", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "captcha_captchacontroller_captchacontroller_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L13", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller", "target": "captcha_captchacontroller_captchacontroller_challenge", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15", "weight": 1.0}, {"source": "captcha_captchacontroller_captchacontroller_challenge", "target": "jsonresponse", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Captcha/CaptchaController.php", "source_location": "L15", "weight": 1.0, "context": "return_type"}], "raw_calls": [{"caller_nid": "captcha_captchacontroller_captchacontroller_challenge", "callee": "createChallenge", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Captcha/CaptchaController.php", "source_location": "L23", "receiver": null}]}
File diff suppressed because one or more lines are too long
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_docs_api_captcha_md", "label": "captcha.md", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_captcha_api_altcha", "label": "Captcha API (ALTCHA)", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L1"}, {"id": "api_captcha_get_api_v1_altcha_challenge", "label": "GET `/api/v1/altcha/challenge`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L10"}, {"id": "api_captcha_response_200", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L14"}, {"id": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "label": "\u0627\u0639\u0645\u0627\u0644 \u06a9\u067e\u0686\u0627 \u0631\u0648\u06cc endpoint\u0647\u0627\u06cc \u0645\u062d\u0627\u0641\u0638\u062a\u200c\u0634\u062f\u0647", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L31"}, {"id": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "label": "\u062e\u0637\u0627\u06cc \u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc \u06a9\u067e\u0686\u0627 `422`", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L51"}, {"id": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "label": "\u0627\u0645\u0646\u06cc\u062a", "file_type": "document", "source_file": "docs/api/captcha.md", "source_location": "L64"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_captcha_md", "target": "api_captcha_captcha_api_altcha", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L1", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_get_api_v1_altcha_challenge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L10", "weight": 1.0}, {"source": "api_captcha_get_api_v1_altcha_challenge", "target": "api_captcha_response_200", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L14", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L31", "weight": 1.0}, {"source": "api_captcha_\u0627\u0639\u0645\u0627\u0644_\u06a9\u067e\u0686\u0627_\u0631\u0648\u06cc_endpoint\u0647\u0627\u06cc_\u0645\u062d\u0627\u0641\u0638\u062a_\u0634\u062f\u0647", "target": "api_captcha_\u062e\u0637\u0627\u06cc_\u0627\u0639\u062a\u0628\u0627\u0631\u0633\u0646\u062c\u06cc_\u06a9\u067e\u0686\u0627_422", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L51", "weight": 1.0}, {"source": "api_captcha_captcha_api_altcha", "target": "api_captcha_\u0627\u0645\u0646\u06cc\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/captcha.md", "source_location": "L64", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "label": "ErrorCodes.php", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L1"}, {"id": "constant_errorcodes_errorcodes", "label": "ErrorCodes", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5"}, {"id": "constant_errorcodes_errorcodes_message", "label": ".message()", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L106"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "target": "constant_errorcodes_errorcodes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5", "weight": 1.0}, {"source": "constant_errorcodes_errorcodes", "target": "constant_errorcodes_errorcodes_message", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L106", "weight": 1.0}], "raw_calls": []}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2359 -885
View File
File diff suppressed because it is too large Load Diff
+62 -22
View File
@@ -450,8 +450,8 @@
"semantic_hash": ""
},
"composer.json": {
"mtime": 1783490678.46749,
"ast_hash": "af0a543bf9cbb00808c5dc8850ca273c",
"mtime": 1783666096.953897,
"ast_hash": "fcf018bd5c7fc59ecd3c76c463e7e9a8",
"semantic_hash": ""
},
"config/bundles.php": {
@@ -780,8 +780,8 @@
"semantic_hash": ""
},
"package.json": {
"mtime": 1783189109.134888,
"ast_hash": "f0b61dd4c429f6bceac993e60b933ab4",
"mtime": 1783666097.0063853,
"ast_hash": "218d7761b7559cdbae136d5d9d8204a8",
"semantic_hash": ""
},
"postcss.config.js": {
@@ -905,8 +905,8 @@
"semantic_hash": ""
},
"src/Auth/Controller/AuthController.php": {
"mtime": 1783071277.1970987,
"ast_hash": "b4778b795ee2d27f5e857a1967c337df",
"mtime": 1783666097.0093415,
"ast_hash": "39b0ff3917eea112c09e8a7b11d7ea1c",
"semantic_hash": ""
},
"src/Auth/Controller/NotificationMobileController.php": {
@@ -915,8 +915,8 @@
"semantic_hash": ""
},
"src/Auth/Controller/PreRegistrationController.php": {
"mtime": 1783238186.8425517,
"ast_hash": "69a551f873ff500c47fc880ea088be8b",
"mtime": 1783666097.0124094,
"ast_hash": "246de59bb3b7acb58a862b66341e0a15",
"semantic_hash": ""
},
"src/Auth/Entity/MobileVerificationOtp.php": {
@@ -1410,7 +1410,7 @@
"semantic_hash": ""
},
"src/Rating/Controller/RatingController.php": {
"mtime": 1782728407.1993864,
"mtime": 1783666764.412828,
"ast_hash": "19a05cd6788d427b52c6c2b604ec69af",
"semantic_hash": ""
},
@@ -1535,8 +1535,8 @@
"semantic_hash": ""
},
"src/Shared/Constant/ErrorCodes.php": {
"mtime": 1782728407.2028558,
"ast_hash": "6555622a4f54511406726cc156eac7aa",
"mtime": 1783666097.015498,
"ast_hash": "6ad487190ee8ba21689c31580d564a4b",
"semantic_hash": ""
},
"src/Shared/Controller/BaseController.php": {
@@ -2185,8 +2185,8 @@
"semantic_hash": ""
},
"README.MD": {
"mtime": 1782933937.9090674,
"ast_hash": "1da6a3afea0f626c55b6f7c1ff126b65",
"mtime": 1783666800.9005868,
"ast_hash": "b63e5ed1aa434fd429f8a04daae5335c",
"semantic_hash": ""
},
"TEST_USERS.md": {
@@ -2195,8 +2195,8 @@
"semantic_hash": ""
},
"config/packages/cache.yaml": {
"mtime": 1781010060.4718235,
"ast_hash": "7e97d477dade0fd454310d84c4a47237",
"mtime": 1783666096.9616337,
"ast_hash": "5a3d4a654c5cc1932cbaa77152f5e9c0",
"semantic_hash": ""
},
"config/packages/debug.yaml": {
@@ -2260,8 +2260,8 @@
"semantic_hash": ""
},
"config/packages/security.yaml": {
"mtime": 1783569345.7766566,
"ast_hash": "6d04ed4f39e92b722ac727559194623a",
"mtime": 1783666096.9829946,
"ast_hash": "7236c28a514152be0d7dbf02834096c4",
"semantic_hash": ""
},
"config/packages/twig.yaml": {
@@ -2300,8 +2300,8 @@
"semantic_hash": ""
},
"config/services.yaml": {
"mtime": 1783570481.7864468,
"ast_hash": "ed9215189d1b96849ae1e9f6dacdc266",
"mtime": 1783666096.987689,
"ast_hash": "92837e221e0914e026f4f79e8e8ee0b6",
"semantic_hash": ""
},
"docs/Architecture_Audit.md": {
@@ -2355,8 +2355,8 @@
"semantic_hash": ""
},
"docs/api/auth.md": {
"mtime": 1783072324.1093419,
"ast_hash": "fe76d2c41f7e9996751b1002edb50c6b",
"mtime": 1783666224.6876435,
"ast_hash": "10113c88f71bb74fb7d6dae80843c253",
"semantic_hash": ""
},
"docs/api/billing.md": {
@@ -2420,7 +2420,7 @@
"semantic_hash": ""
},
"docs/api/rating.md": {
"mtime": 1782728407.1106706,
"mtime": 1783666791.9281247,
"ast_hash": "ab1f1c46ee73542ebd50f8eddc428efb",
"semantic_hash": ""
},
@@ -3803,5 +3803,45 @@
"mtime": 1783570038.522232,
"ast_hash": "0bc13f299e24b463b720695195298c6a",
"semantic_hash": ""
},
"assets/admin/components/ui/Altcha.tsx": {
"mtime": 1783666097.1123717,
"ast_hash": "76bfa4d9d7c539585eb8b3836c81f3cb",
"semantic_hash": ""
},
"src/Shared/Captcha/AltchaService.php": {
"mtime": 1783666097.1130326,
"ast_hash": "432b4dd0304710632ba86b243e530a5b",
"semantic_hash": ""
},
"src/Shared/Captcha/CaptchaController.php": {
"mtime": 1783666097.1135097,
"ast_hash": "e6fccf870e0891e4d836baf6c27aa958",
"semantic_hash": ""
},
"src/Shared/Captcha/CaptchaGuard.php": {
"mtime": 1783666097.1140714,
"ast_hash": "a84bc84f26a9296f39d446336abd45d5",
"semantic_hash": ""
},
"tests/Shared/Captcha/AltchaServiceTest.php": {
"mtime": 1783666097.1154962,
"ast_hash": "f6a6ca5f5ccaa6fb6cbf811f1e91dc0b",
"semantic_hash": ""
},
"tests/Shared/Captcha/CaptchaFlowTest.php": {
"mtime": 1783666097.1165395,
"ast_hash": "86aa236ea6e3bb4067b6511b7161661f",
"semantic_hash": ""
},
".claude/prompt/altcha-captcha-integration.md": {
"mtime": 1783666097.111438,
"ast_hash": "312b9b9b16b99ac3550f2911e34619e8",
"semantic_hash": ""
},
"docs/api/captcha.md": {
"mtime": 1783666775.6831665,
"ast_hash": "4796a4acacfaba72c1e4b741b234c96f",
"semantic_hash": ""
}
}
+63 -40
View File
@@ -1,5 +1,5 @@
{
"name": "html",
"name": "clinicpro",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -14,10 +14,12 @@
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-table": "^8.0.0",
"@types/leaflet": "^1.9.21",
"altcha": "^3.2.0",
"jalaali-js": "^1.2.8",
"leaflet": "^1.9.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-easy-crop": "^6.1.0",
"react-hook-form": "^7.0.0",
"react-hot-toast": "^2.0.0",
"react-leaflet": "^5.0.0",
@@ -15260,6 +15262,7 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15277,6 +15280,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15294,6 +15298,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15311,6 +15316,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15328,6 +15334,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15345,6 +15352,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15362,6 +15370,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15379,6 +15388,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15396,6 +15406,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15413,6 +15424,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15430,6 +15442,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15447,6 +15460,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15464,6 +15478,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15481,6 +15496,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15498,6 +15514,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15515,6 +15532,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15532,6 +15550,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15549,6 +15568,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15566,6 +15586,7 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15583,6 +15604,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15600,6 +15622,7 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15617,6 +15640,7 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15634,6 +15658,7 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15651,6 +15676,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15668,6 +15694,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -15685,6 +15712,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -16072,9 +16100,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16089,9 +16114,6 @@
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16106,9 +16128,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16123,9 +16142,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16140,9 +16156,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16157,9 +16170,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16174,9 +16184,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16191,9 +16198,6 @@
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16208,9 +16212,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16225,9 +16226,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16242,9 +16240,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16259,9 +16254,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -16276,9 +16268,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -17797,6 +17786,15 @@
"ajv": "^8.8.2"
}
},
"node_modules/altcha": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/altcha/-/altcha-3.2.0.tgz",
"integrity": "sha512-wxOTBXigI5SeMjkpA4RhVZifTJr0HYCiVnbQHrvcg5u8XS+1hyKuDs1JE4XsaElpaIFtvmw2ami5dgseysr54w==",
"license": "MIT",
"dependencies": {
"hash-wasm": "^4.12.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -20245,6 +20243,12 @@
"node": ">=8"
}
},
"node_modules/hash-wasm": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz",
"integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==",
"license": "MIT"
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -22372,6 +22376,12 @@
"node": ">=18"
}
},
"node_modules/normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
"license": "BSD-3-Clause"
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -23350,6 +23360,19 @@
"react": "^19.2.7"
}
},
"node_modules/react-easy-crop": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-6.2.2.tgz",
"integrity": "sha512-b0HOicSvLYoNk1yvZTwjH8sNjF5uD9xLtWrSDnv+fx1dXTbwd8bNLQaP6gjXw//A+tni9Mw5KDmPNOEjKF55NQ==",
"license": "MIT",
"dependencies": {
"normalize-wheel": "^1.0.1"
},
"peerDependencies": {
"react": ">=16.4.0",
"react-dom": ">=16.4.0"
}
},
"node_modules/react-hook-form": {
"version": "7.78.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.78.0.tgz",
+1
View File
@@ -39,6 +39,7 @@
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-table": "^8.0.0",
"@types/leaflet": "^1.9.21",
"altcha": "^3.2.0",
"jalaali-js": "^1.2.8",
"leaflet": "^1.9.4",
"react": "^19.0.0",
+10
View File
@@ -11,6 +11,7 @@ use App\Auth\Service\TokenService;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Captcha\CaptchaGuard;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use Doctrine\ORM\EntityManagerInterface;
@@ -40,6 +41,7 @@ class AuthController extends BaseController
private readonly UserActiveContextRepository $contextRepo,
private readonly UserPasswordHasherInterface $hasher,
private readonly EntityManagerInterface $em,
private readonly CaptchaGuard $captcha,
) {}
/**
@@ -140,6 +142,8 @@ class AuthController extends BaseController
return $this->error(ErrorCodes::ERR_RATE_LIMIT_001, ErrorCodes::message(ErrorCodes::ERR_RATE_LIMIT_001), 429);
}
$this->captcha->assertValid($request);
$data = json_decode($request->getContent(), true) ?? [];
$mobile = trim($data['mobile'] ?? '');
$domain = isset($data['domain']) ? substr(trim((string) $data['domain']), 0, 253) : null;
@@ -275,6 +279,8 @@ class AuthController extends BaseController
#[Route('/api/v1/user/register', methods: ['POST'])]
public function register(Request $request): JsonResponse
{
$this->captcha->assertValid($request);
$data = json_decode($request->getContent(), true) ?? [];
$grant = trim($data['grant'] ?? '');
$realName = trim($data['real_name'] ?? '');
@@ -375,6 +381,8 @@ class AuthController extends BaseController
return $resp;
}
$this->captcha->assertValid($request);
$data = json_decode($request->getContent(), true) ?? [];
$grant = trim($data['grant'] ?? '');
@@ -399,6 +407,8 @@ class AuthController extends BaseController
return $resp;
}
$this->captcha->assertValid($request);
$data = json_decode($request->getContent(), true) ?? [];
$grant = trim($data['grant'] ?? '');
$newPassword = trim($data['new_password'] ?? '');
@@ -10,6 +10,7 @@ use App\Clinic\Entity\Clinic;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Entity\Doctor;
use App\Doctor\Repository\DoctorRepository;
use App\Shared\Captcha\CaptchaGuard;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Sms\Service\SmsService;
@@ -35,11 +36,14 @@ class PreRegistrationController extends BaseController
private readonly SmsService $sms,
private readonly LoggerInterface $logger,
private readonly string $appUrl,
private readonly CaptchaGuard $captcha,
) {}
#[Route('/api/v1/pre-registration', methods: ['POST'])]
public function submit(Request $request): JsonResponse
{
$this->captcha->assertValid($request);
$data = json_decode($request->getContent(), true) ?? [];
$type = trim($data['type'] ?? '');
$name = trim($data['name'] ?? '');
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Shared\Captcha;
use AltchaOrg\Altcha\V1\Altcha;
use AltchaOrg\Altcha\V1\ChallengeOptions;
use Psr\Cache\CacheItemPoolInterface;
/**
* ALTCHA self-hosted proof-of-work captcha.
*
* Challenges are HMAC-signed with the server secret and carry an embedded
* `expires` timestamp. A solved payload is accepted at most once: its signature
* is burned in Redis for the remaining lifetime of the challenge, so a captured
* payload cannot be replayed.
*/
class AltchaService
{
private readonly Altcha $altcha;
public function __construct(
private readonly string $hmacKey,
private readonly bool $enabled,
private readonly int $maxNumber,
private readonly int $expireSeconds,
private readonly CacheItemPoolInterface $altchaPool,
) {
$this->altcha = new Altcha($this->hmacKey);
}
public function enabled(): bool
{
return $this->enabled;
}
/**
* Build a fresh signed challenge for the widget.
*
* @return array<string, string|int> keys: algorithm, challenge, maxnumber, salt, signature
*/
public function createChallenge(): array
{
$challenge = $this->altcha->createChallenge(new ChallengeOptions(
maxNumber: $this->maxNumber,
expires: (new \DateTimeImmutable())->add(new \DateInterval('PT' . $this->expireSeconds . 'S')),
));
return [
'algorithm' => $challenge->algorithm,
'challenge' => $challenge->challenge,
'maxnumber' => $challenge->maxNumber,
'salt' => $challenge->salt,
'signature' => $challenge->signature,
];
}
/**
* Verify a base64 solution payload sent by the client.
* Returns false on any invalid/expired/replayed payload.
*/
public function verifySolution(string $payloadBase64): bool
{
if ($payloadBase64 === '' || !$this->altcha->verifySolution($payloadBase64, true)) {
return false;
}
return $this->consumeOnce($payloadBase64);
}
/**
* Atomically burn the challenge signature so it can be used only once.
* Returns false if this signature has already been consumed.
*/
private function consumeOnce(string $payloadBase64): bool
{
$decoded = json_decode((string) base64_decode($payloadBase64, true), true);
$signature = is_array($decoded) ? ($decoded['signature'] ?? null) : null;
if (!is_string($signature) || $signature === '') {
return false;
}
$item = $this->altchaPool->getItem('altcha_used_' . $signature);
if ($item->isHit()) {
return false;
}
$item->set(true)->expiresAfter($this->expireSeconds);
$this->altchaPool->save($item);
return true;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Shared\Captcha;
use App\Shared\Controller\BaseController;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
#[OA\Tag(name: 'Captcha')]
class CaptchaController extends BaseController
{
public function __construct(private readonly AltchaService $altcha) {}
#[OA\Get(
path: '/api/v1/altcha/challenge',
summary: 'صدور یک challenge امضاشده‌ی ALTCHA برای حل proof-of-work سمت مرورگر',
responses: [new OA\Response(response: 200, description: 'ALTCHA challenge object')]
)]
#[Route('/api/v1/altcha/challenge', methods: ['GET'])]
public function challenge(): JsonResponse
{
return new JsonResponse($this->altcha->createChallenge());
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Shared\Captcha;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Symfony\Component\HttpFoundation\Request;
/**
* Drop-in captcha check for public endpoints.
*
* Call `assertValid($request)` at the top of any unauthenticated POST handler.
* No-op when ALTCHA is disabled (dev/test), so protected handlers stay testable
* without solving a proof-of-work.
*/
class CaptchaGuard
{
public function __construct(private readonly AltchaService $altcha) {}
public function assertValid(Request $request): void
{
if (!$this->altcha->enabled()) {
return;
}
$data = json_decode($request->getContent(), true);
$payload = is_array($data) ? (string) ($data['altcha'] ?? '') : '';
if (!$this->altcha->verifySolution($payload)) {
throw new AppException(ErrorCodes::ERR_CAPTCHA_001, null, 422, 'altcha');
}
}
}
+4
View File
@@ -72,6 +72,9 @@ class ErrorCodes
// Rate Limit
public const ERR_RATE_LIMIT_001 = 'ERR_RATE_LIMIT_001';
// Captcha (ALTCHA)
public const ERR_CAPTCHA_001 = 'ERR_CAPTCHA_001';
// Rating
public const ERR_RATING_NOT_ELIGIBLE = 'ERR_RATING_NOT_ELIGIBLE';
@@ -126,6 +129,7 @@ class ErrorCodes
self::ERR_SECRETARY_001 => 'پلن فعلی اجازه منشی بیشتر را نمی‌دهد',
self::ERR_CONFLICT_001 => 'تداخل: منبع در حال استفاده است یا قبلاً تغییر کرده است',
self::ERR_RATE_LIMIT_001 => 'درخواست‌های زیاد. لطفاً بعداً تلاش کنید',
self::ERR_CAPTCHA_001 => 'تأیید امنیتی ناموفق بود. لطفاً صفحه را رفرش کنید و دوباره تلاش کنید',
self::ERR_STAFF_NOT_FOUND => 'پرسنل یافت نشد',
self::ERR_SUBSCRIPTION_REQUIRED => 'این قابلیت نیاز به پنل Basic یا بالاتر دارد',
self::ERR_TRIAL_ALREADY_USED => 'قبلاً از تریال استفاده کرده‌اید',
@@ -0,0 +1,93 @@
<?php
namespace App\Tests\Shared\Captcha;
use AltchaOrg\Altcha\V1\Altcha;
use AltchaOrg\Altcha\V1\Hasher\Algorithm;
use App\Shared\Captcha\AltchaService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
/**
* ALTCHA service: signed-challenge issuing, solution verification, one-time replay guard.
*/
class AltchaServiceTest extends TestCase
{
private const KEY = 'test-hmac-key-please-change';
private function service(bool $enabled = true, int $maxNumber = 2000, int $expire = 300): AltchaService
{
return new AltchaService(self::KEY, $enabled, $maxNumber, $expire, new ArrayAdapter());
}
/**
* Solve a service-issued challenge the way the browser widget would, and
* return the base64 payload the client sends back.
*/
private function solvedPayload(array $challenge): string
{
$solver = new Altcha(self::KEY);
$solution = $solver->solveChallenge(
$challenge['challenge'],
$challenge['salt'],
Algorithm::SHA256,
(int) $challenge['maxnumber'],
);
self::assertNotNull($solution, 'challenge must be solvable');
return base64_encode(json_encode([
'algorithm' => $challenge['algorithm'],
'challenge' => $challenge['challenge'],
'number' => $solution->number,
'salt' => $challenge['salt'],
'signature' => $challenge['signature'],
]));
}
public function testCreateChallengeShape(): void
{
$c = $this->service()->createChallenge();
self::assertSame('SHA-256', $c['algorithm']);
self::assertArrayHasKey('challenge', $c);
self::assertArrayHasKey('salt', $c);
self::assertArrayHasKey('signature', $c);
self::assertSame(2000, $c['maxnumber']);
}
public function testValidSolutionVerifies(): void
{
$svc = $this->service();
$payload = $this->solvedPayload($svc->createChallenge());
self::assertTrue($svc->verifySolution($payload));
}
public function testReplayIsRejected(): void
{
$svc = $this->service();
$payload = $this->solvedPayload($svc->createChallenge());
self::assertTrue($svc->verifySolution($payload), 'first use accepted');
self::assertFalse($svc->verifySolution($payload), 'second use (replay) rejected');
}
public function testTamperedSignatureRejected(): void
{
$svc = $this->service();
$challenge = $svc->createChallenge();
$challenge['signature'] = str_repeat('0', strlen($challenge['signature']));
self::assertFalse($svc->verifySolution($this->solvedPayload($challenge)));
}
public function testEmptyAndGarbagePayloadsRejected(): void
{
$svc = $this->service();
self::assertFalse($svc->verifySolution(''));
self::assertFalse($svc->verifySolution('not-base64-!@#'));
self::assertFalse($svc->verifySolution(base64_encode('{"foo":"bar"}')));
}
public function testEnabledFlag(): void
{
self::assertTrue($this->service(true)->enabled());
self::assertFalse($this->service(false)->enabled());
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Tests\Shared\Captcha;
use App\Tests\ApiTestCase;
/**
* ALTCHA HTTP surface: public challenge endpoint, and captcha bypass on public
* endpoints while ALTCHA is disabled (the default in the test environment).
*/
class CaptchaFlowTest extends ApiTestCase
{
public function testChallengeEndpointIsPublicAndWellFormed(): void
{
$this->client->request('GET', '/api/v1/altcha/challenge');
self::assertSame(200, $this->responseCode());
$body = json_decode($this->client->getResponse()->getContent(), true);
self::assertSame('SHA-256', $body['algorithm']);
foreach (['challenge', 'salt', 'signature', 'maxnumber'] as $key) {
self::assertArrayHasKey($key, $body);
}
}
public function testPublicEndpointBypassesCaptchaWhenDisabled(): void
{
// ALTCHA_ENABLED is false in test → guard is a no-op, so send-code proceeds
// past the captcha check without an `altcha` field (fails later on validation only).
$this->client->request(
'POST',
'/api/v1/user/send-code',
server: ['CONTENT_TYPE' => 'application/json'],
content: json_encode(['mobile' => '09123456789']),
);
// Not a 422 captcha rejection: either success or a non-captcha error.
$body = json_decode($this->client->getResponse()->getContent(), true) ?? [];
$code = $body['errors'][0]['code'] ?? null;
self::assertNotSame('ERR_CAPTCHA_001', $code);
}
}
+2202 -426
View File
File diff suppressed because it is too large Load Diff