Files
portal/docs/superpowers/specs/2026-06-21-m2-smartcaptcha-frontend-widget-design.md
T

6.3 KiB
Raw Blame History

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

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


Проблема

RegisterView.vue:70 шлёт фейковый 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', '').
    • onUnmountedwindow.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 (новый)

/// <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. Реальный виджет проверяется на проде после ключа.