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:
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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 لازم است.
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 موجود است، نه جایگزین آن.
|
||||
@@ -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
@@ -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?**
|
||||
|
||||
graphify-out/cache/ast/v0.8.44/0849c4c39bf752e5f94477e82ef9080245cde06f6fc59bac80a5b5a0f0f7eecc.json
Vendored
+1
@@ -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}]}
|
||||
graphify-out/cache/ast/v0.8.44/0f897db099d2ec763d97567742429eb682a7b6f955187cd2382b2df8957bd09a.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/1e979b2ea4c50ef051443cc5bd9f5351ac5cc9d8934c67c0c0319e3b36fc3d8b.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/51e53996bd81dece6745b657f1728dd6e02e1dcafe6171f84761e9624dcd0f60.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/546ab73e38144470b6ba0ee685ce273acc48d22a03f7d3f58c4e9d3cc9213230.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/63bfddb2b2dd14542521febeda5cc3584ed8c08eb27e1983dfeb1657154b3f7f.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/63d566ddb57c57860dce114a2fa5575f83877ad50bcdaace4d2f31648f83aa6c.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/6abeef1dc282dce2ff64bfc1ae4ea098c41f2fea0f3be9dfc38be3ccd7c628a5.json
Vendored
+1
@@ -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}
|
||||
graphify-out/cache/ast/v0.8.44/962704b45cef9adf358f089e6ca1282c649de48375527eaa85ccecf1ecd9ab63.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/a1dbde0191ac9e7b42cf608b82c8397c0425a57300c749f29f5589e4d99a82fa.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/b1602ecd3be5e3c1e29eb2f66b7beb9f991e5d7ac487e72e0c6bf6cf640a3879.json
Vendored
+1
@@ -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}]}
|
||||
graphify-out/cache/ast/v0.8.44/c2185822b43db5ca6a54b83767776d61b6602130d3d66920219a7ee30faeeeaf.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/c435895b86893a68c87144f4f70cb4e75f26bfbfc99390769c837c450dfb3a21.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/d98e311e0268ef2b6015b051d42bfedaaf9afa0a2850d7842f1e0d7600916f4a.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/db00decdd07f5ce818cae523811ea83bb924334ebf86272b975dfec118027095.json
Vendored
+1
@@ -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}
|
||||
graphify-out/cache/ast/v0.8.44/f786ca8be38675ac8bf6c0dc71c67a842d6785a4de2a7c3294b6df6d082284ef.json
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2359
-885
File diff suppressed because it is too large
Load Diff
+62
-22
@@ -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": ""
|
||||
}
|
||||
}
|
||||
Generated
+63
-40
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'] ?? '');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user