# Captcha API (ALTCHA)

کپچای خودمیزبان مبتنی بر ALTCHA (Proof-of-Work، بدون تصویر، بدون سرویس خارجی). برای محافظت از endpointهای عمومی و بدون احراز هویت در برابر اسپم/بات.

- فعال/غیرفعال از دو جا کنترل می‌شود:
  1. **پنل ادمین** (`/admin/settings` → عمومی → کپچای امنیتی) — کلید `altcha_enabled` در `site_config`.
  2. **متغیر محیطی** `ALTCHA_ENABLED` (پیش‌فرض/fallback).
  - **اولویت:** اگر کلید پنل ست شده باشد مقدم است؛ در غیر این صورت به `ALTCHA_ENABLED` برمی‌گردد.
- وقتی غیرفعال است، همه‌ی endpointها بدون فیلد `altcha` کار می‌کنند و کلاینت‌ها (از `/altcha/config`) widget را نشان نمی‌دهند.

---

## 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 استاندارد).

---

## GET `/api/v1/altcha/config`

وضعیت فعال بودن کپچا. **عمومی**. کلاینت‌ها قبل از رندر widget این را می‌خوانند؛ اگر `enabled=false` باشد، هیچ widgetی نشان داده نمی‌شود و فرم‌ها بدون فیلد `altcha` ارسال می‌شوند (بک‌اند هم no-op است).

### Response `200`
```json
{ "enabled": false }
```

---

## اعمال کپچا روی 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 |
| `/api/v1/user/login` | POST |

> ورود با رمز عبور (`/api/v1/user/login`) توسط `PasswordAuthenticator` قبل از controller intercept می‌شود؛ کپچا داخل `authenticate()` (بعد از rate-limit) با `CaptchaGuard::assertValid()` بررسی می‌شود.

> 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 موجود است، نه جایگزین آن.
- **CSP:** widget حل proof-of-work را داخل Web Workerهای `blob:` انجام می‌دهد؛ CSP پنل ادمین (`AdminCspSubscriber`) باید `worker-src 'self' blob:` داشته باشد، وگرنه widget با `state=error` می‌ماند و کپچا هرگز حل نمی‌شود.
