Files
clinicpro/.claude/prompt/altcha-captcha-integration.md
T
hamed 10b0743d9a 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.
2026-07-10 10:31:59 +03:30

180 lines
13 KiB
Markdown
Raw Blame History

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