# M-2 фронт — виджет Yandex SmartCaptcha на регистрации

**Дата:** 21.06.2026. **Кодовая фраза стены:** «роутер-наставник».
**Статус:** дизайн одобрен владельцем. Следующий шаг — план → реализация.
**Связь:** завершает M-2 ([backend-spec](2026-06-21-m2-yandex-smartcaptcha-design.md)). Бэкенд-драйвер `YandexSmartCaptchaVerifier` готов (коммит `12eace36`); нужен реальный фронт-виджет, выдающий настоящий токен.

---

## Проблема

[RegisterView.vue:70](../../../app/resources/js/views/auth/RegisterView.vue#L70) шлёт фейковый `captcha_token: 'dev-captcha-stub'` от чекбокса «не робот». С реальным бэкенд-драйвером (`CAPTCHA_DRIVER=yandex`) этот стаб Yandex отвергнет → регистрация сломается. Нужен настоящий виджет SmartCaptcha, отдающий валидный токен.

Интеграция (сверено по докам Yandex 2025):
- Скрипт: `https://smartcaptcha.cloud.yandex.ru/captcha.js?render=onload&onload=<fn>` (defer).
- `window.smartCaptcha.render(container, { sitekey, callback, hl })` → возвращает `widgetId`; `callback(token: string)` отдаёт токен.
- `window.smartCaptcha.reset(widgetId)` — сброс после ошибки.

---

## Решение

### 1. `app/resources/js/components/auth/SmartCaptchaWidget.vue` (новый)

Изолированный компонент, `v-model` = строка-токен.

**Props/emits:** `modelValue: string`; `emit('update:modelValue', token)`. `expose({ reset })`.

**Логика:**
- `sitekey = import.meta.env.VITE_YANDEX_SMARTCAPTCHA_SITEKEY ?? ''`.
- **sitekey задан** (прод):
  - `onMounted` → если `captcha.js` ещё не в DOM, добавить `<script defer src="…/captcha.js?render=onload&onload=__lidSmartCaptchaOnload">`; иначе сразу рендерить.
  - В `__lidSmartCaptchaOnload` (или при уже-готовом `window.smartCaptcha`): `widgetId = window.smartCaptcha.render(container, { sitekey, hl: 'ru', callback: (t) => emit('update:modelValue', t) })`.
  - `reset()` → `window.smartCaptcha.reset(widgetId)` + `emit('update:modelValue', '')`.
  - `onUnmounted` → `window.smartCaptcha.destroy?.(widgetId)` (best-effort).
  - Контейнер — `<div ref="container" />`.
- **sitekey пуст** (dev/local/тесты): fallback — Vuetify `<v-checkbox>` «Подтвердите, что вы не робот»; отмечен → `emit('update:modelValue', 'dev-captcha-stub')`, снят → `''`. `reset()` → снять. Так dev и существующие сценарии не ломаются.

### 2. `app/resources/js/views/auth/RegisterView.vue` (правка)

- Удалить локальный чекбокс `captchaAccepted` + стаб-логику (строки ~23, 161-171, 68-70).
- `const captchaToken = ref('')`; `const captchaRef = ref()`.
- В шаблоне на месте чекбокса: `<SmartCaptchaWidget v-model="captchaToken" ref="captchaRef" :error-messages="errors.captcha_token" />` (error прокинуть в fallback-чекбокс; у реального виджета ошибки нет).
- `canSubmit` → заменить `captchaAccepted.value` на `captchaToken.value.length > 0`.
- `register({ …, captcha_token: captchaToken.value })`.
- В `catch` после ошибки: `captchaRef.value?.reset()` (одноразовый токен Yandex истёк/использован — нужен свежий).

### 3. `app/resources/js/vite-env.d.ts` (новый)

```ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
    readonly VITE_YANDEX_SMARTCAPTCHA_SITEKEY?: string;
}
interface ImportMeta {
    readonly env: ImportMetaEnv;
}

interface SmartCaptchaRenderParams {
    sitekey: string;
    callback?: (token: string) => void;
    hl?: string;
    invisible?: boolean;
}
interface SmartCaptcha {
    render(container: HTMLElement | string, params: SmartCaptchaRenderParams): number;
    reset(widgetId?: number): void;
    destroy?(widgetId?: number): void;
    execute?(widgetId?: number): void;
}
interface Window {
    smartCaptcha?: SmartCaptcha;
    __lidSmartCaptchaOnload?: () => void;
}
```

### 4. `app/.env.example` (правка)

Рядом с `VITE_APP_NAME`: `VITE_YANDEX_SMARTCAPTCHA_SITEKEY=` (на проде — клиентский ключ `ysc1_…`).

---

## Границы / YAGNI

- **Видимая** капча (не invisible) — привычно, как текущий «не робот», проще.
- **Без бэкенда/схемы** — только фронт; бэкенд-драйвер уже готов.
- Fallback на стаб при пустом sitekey — чтобы dev/local/CI и существующие тесты/прогоны не падали.
- CSP: домен `smartcaptcha.cloud.yandex.ru` нужно добавить в `script-src`/`connect-src`/`frame-src` прод-nginx CSP — **операционный шаг владельца при активации** (вне фронт-кода), отметить в деплое.

## Деплой (шаг владельца)

1. Создать капчу в Yandex Cloud → взять клиентский (`ysc1_`) и серверный (`ysc2_`) ключи.
2. Прод-`.env`: `VITE_YANDEX_SMARTCAPTCHA_SITEKEY=ysc1_…` (фронт-сборка) + `YANDEX_SMARTCAPTCHA_SERVER_KEY=ysc2_…` + `CAPTCHA_DRIVER=yandex`.
3. Расширить CSP в nginx доменом `smartcaptcha.cloud.yandex.ru` (`script-src`/`connect-src`/`frame-src`).
4. Пересобрать фронт (`npm run build`) + `config:cache`. Капча активна end-to-end.

## Verification (честно)

- vue-tsc `npm run type-check` = 0.
- `npm run build` (vite) — сборка проходит, бандл собран.
- eslint/prettier — чисто.
- **vitest в проекте сломан** → юнит-тестами компонент не прогнать; корректность fallback-ветки и типов покрыта type-check + build. Реальный виджет проверяется на проде после ключа.
