2026-05-08 17:09:56 +03:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Экран регистрации (RegisterView).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Источник дизайна: liderra_v8_handoff/concepts/v8_login.html секция #form-register.
|
|
|
|
|
|
* Источник логики: ТЗ v8.5 §1.5/§4.1 — два обязательных click-wrap'а
|
|
|
|
|
|
* (оферта + согласие на ПДн). 3-й «маркетинговый» click-wrap из handoff
|
|
|
|
|
|
* НЕ реализован (handoff противоречит ТЗ — расхождение #2 из реестра v1.13).
|
|
|
|
|
|
*
|
2026-06-18 23:33:26 +03:00
|
|
|
|
* G1/SP3a: подключён к новому backend-потоку — register создаёт тенант в
|
|
|
|
|
|
* pending_email_confirm и шлёт 6-значный код; после успеха ведём на /confirm-email.
|
2026-05-08 17:09:56 +03:00
|
|
|
|
*/
|
2026-06-21 09:34:31 +03:00
|
|
|
|
import SmartCaptchaWidget from '../../components/auth/SmartCaptchaWidget.vue';
|
2026-05-08 19:59:43 +03:00
|
|
|
|
import { extractValidationErrors } from '../../api/client';
|
|
|
|
|
|
import { useAuthStore } from '../../stores/auth';
|
2026-05-08 17:09:56 +03:00
|
|
|
|
import { computed, ref } from 'vue';
|
2026-05-08 19:59:43 +03:00
|
|
|
|
import { useRouter } from 'vue-router';
|
2026-05-08 17:09:56 +03:00
|
|
|
|
|
|
|
|
|
|
const email = ref('');
|
|
|
|
|
|
const password = ref('');
|
|
|
|
|
|
const showPassword = ref(false);
|
|
|
|
|
|
const acceptOffer = ref(false);
|
|
|
|
|
|
const acceptPdn = ref(false);
|
2026-06-21 09:34:31 +03:00
|
|
|
|
const captchaToken = ref('');
|
|
|
|
|
|
const captchaRef = ref<InstanceType<typeof SmartCaptchaWidget> | null>(null);
|
2026-05-08 19:59:43 +03:00
|
|
|
|
const errors = ref<Record<string, string[]>>({});
|
|
|
|
|
|
|
|
|
|
|
|
const auth = useAuthStore();
|
|
|
|
|
|
const router = useRouter();
|
2026-05-08 17:09:56 +03:00
|
|
|
|
|
2026-06-24 16:48:35 +03:00
|
|
|
|
// §6: частые/словарные пароли (password123, qwerty, 123456…) — всегда «Слабый»,
|
|
|
|
|
|
// даже если формально длинные и с цифрой. База — самые ходовые + клавиатурные ряды.
|
|
|
|
|
|
const COMMON_WEAK =
|
|
|
|
|
|
/^(password|passw0rd|qwerty|qwertyuiop|qazwsx|123456|1234567|12345678|123456789|1234567890|111111|000000|123123|iloveyou|admin|welcome|abc123|letmein|monkey|dragon|master|login|sunshine|football|password1|пароль|йцукен|qwerty123)\d*$/i;
|
|
|
|
|
|
|
2026-05-08 17:09:56 +03:00
|
|
|
|
// Простая оценка силы пароля 0..4 для индикатора. На backend будет zxcvbn.
|
|
|
|
|
|
const passwordStrength = computed(() => {
|
|
|
|
|
|
const v = password.value;
|
|
|
|
|
|
if (!v) return 0;
|
2026-06-24 16:48:35 +03:00
|
|
|
|
if (COMMON_WEAK.test(v)) return 1; // частый словарный пароль — слабый независимо от длины/цифр
|
2026-05-08 17:09:56 +03:00
|
|
|
|
let score = 0;
|
|
|
|
|
|
if (v.length >= 8) score++;
|
|
|
|
|
|
if (/[A-ZА-Я]/.test(v) && /[a-zа-я]/.test(v)) score++;
|
|
|
|
|
|
if (/\d/.test(v)) score++;
|
|
|
|
|
|
if (/[^A-Za-zА-Яа-я0-9]/.test(v)) score++;
|
|
|
|
|
|
return score;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const strengthLabel = computed(() => {
|
|
|
|
|
|
const map = ['—', 'Слабый', 'Средний', 'Хороший', 'Надёжный'];
|
|
|
|
|
|
return map[passwordStrength.value];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const strengthColor = computed(() => {
|
|
|
|
|
|
const map = ['', 'error', 'warning', 'info', 'success'];
|
|
|
|
|
|
return map[passwordStrength.value];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const canSubmit = computed(
|
2026-06-18 23:33:26 +03:00
|
|
|
|
() =>
|
|
|
|
|
|
email.value.length > 0 &&
|
|
|
|
|
|
password.value.length >= 8 &&
|
|
|
|
|
|
acceptOffer.value &&
|
|
|
|
|
|
acceptPdn.value &&
|
2026-06-21 09:34:31 +03:00
|
|
|
|
captchaToken.value.length > 0,
|
2026-05-08 17:09:56 +03:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-08 19:59:43 +03:00
|
|
|
|
async function handleSubmit() {
|
|
|
|
|
|
errors.value = {};
|
|
|
|
|
|
try {
|
2026-06-18 23:33:26 +03:00
|
|
|
|
await auth.register({
|
2026-05-08 19:59:43 +03:00
|
|
|
|
email: email.value,
|
|
|
|
|
|
password: password.value,
|
|
|
|
|
|
accept_offer: acceptOffer.value,
|
|
|
|
|
|
accept_pdn: acceptPdn.value,
|
2026-06-21 09:34:31 +03:00
|
|
|
|
// Токен от SmartCaptchaWidget: реальный Yandex-токен (prod) или
|
|
|
|
|
|
// 'dev-captcha-stub' (dev/local без sitekey, fallback-ветка виджета).
|
|
|
|
|
|
captcha_token: captchaToken.value,
|
2026-05-08 19:59:43 +03:00
|
|
|
|
});
|
2026-06-18 23:33:26 +03:00
|
|
|
|
await router.push('/confirm-email');
|
2026-05-08 19:59:43 +03:00
|
|
|
|
} catch (error: unknown) {
|
2026-06-21 09:34:31 +03:00
|
|
|
|
// Одноразовый Yandex-токен использован/истёк — сбрасываем виджет под новую попытку.
|
|
|
|
|
|
captchaRef.value?.reset();
|
2026-05-08 19:59:43 +03:00
|
|
|
|
const validationErrors = extractValidationErrors(error);
|
|
|
|
|
|
if (validationErrors) {
|
|
|
|
|
|
errors.value = validationErrors;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errors.value = { email: ['Произошла ошибка. Попробуйте позже.'] };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-08 17:09:56 +03:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<v-card variant="flat" :max-width="380" width="100%" color="transparent" class="register-card">
|
|
|
|
|
|
<header class="register-header">
|
|
|
|
|
|
<h1 class="text-h5 mb-1">Создать аккаунт</h1>
|
|
|
|
|
|
<p class="text-body-2 text-medium-emphasis ma-0">
|
|
|
|
|
|
Уже есть?
|
|
|
|
|
|
<RouterLink to="/login" class="text-primary"> Войдите </RouterLink>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<v-form class="register-form" @submit.prevent="handleSubmit">
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="email"
|
|
|
|
|
|
label="Рабочий email"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
autocomplete="email"
|
|
|
|
|
|
placeholder="manager@yourcompany.ru"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
required
|
2026-05-08 19:59:43 +03:00
|
|
|
|
:error-messages="errors.email"
|
2026-05-08 17:09:56 +03:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<v-text-field
|
|
|
|
|
|
v-model="password"
|
|
|
|
|
|
label="Пароль"
|
|
|
|
|
|
:type="showPassword ? 'text' : 'password'"
|
|
|
|
|
|
autocomplete="new-password"
|
|
|
|
|
|
placeholder="Минимум 8 символов"
|
|
|
|
|
|
variant="outlined"
|
|
|
|
|
|
density="comfortable"
|
|
|
|
|
|
required
|
2026-05-08 19:59:43 +03:00
|
|
|
|
:error-messages="errors.password"
|
2026-05-17 07:40:46 +03:00
|
|
|
|
>
|
|
|
|
|
|
<template #append-inner>
|
|
|
|
|
|
<v-icon
|
2026-05-17 07:45:48 +03:00
|
|
|
|
class="password-toggle"
|
2026-05-17 07:40:46 +03:00
|
|
|
|
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
|
|
|
|
|
:aria-label="showPassword ? 'Скрыть пароль' : 'Показать пароль'"
|
|
|
|
|
|
role="button"
|
|
|
|
|
|
tabindex="0"
|
|
|
|
|
|
@click="showPassword = !showPassword"
|
|
|
|
|
|
@keydown.enter.prevent="showPassword = !showPassword"
|
|
|
|
|
|
@keydown.space.prevent="showPassword = !showPassword"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</v-text-field>
|
2026-05-08 17:09:56 +03:00
|
|
|
|
|
|
|
|
|
|
<div v-if="password" class="strength-block mb-2">
|
|
|
|
|
|
<v-progress-linear
|
|
|
|
|
|
:model-value="(passwordStrength / 4) * 100"
|
|
|
|
|
|
:color="strengthColor"
|
|
|
|
|
|
height="4"
|
|
|
|
|
|
rounded
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-caption text-medium-emphasis"> {{ strengthLabel }} </span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="checks">
|
|
|
|
|
|
<v-checkbox v-model="acceptOffer" density="compact" hide-details color="primary">
|
|
|
|
|
|
<template #label>
|
|
|
|
|
|
<span class="text-body-2">
|
|
|
|
|
|
Принимаю
|
|
|
|
|
|
<a href="/legal/offer" class="text-primary" target="_blank" rel="noopener">оферту</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</v-checkbox>
|
|
|
|
|
|
<v-checkbox v-model="acceptPdn" density="compact" hide-details color="primary">
|
|
|
|
|
|
<template #label>
|
|
|
|
|
|
<span class="text-body-2">
|
|
|
|
|
|
Согласен с
|
|
|
|
|
|
<a href="/legal/privacy" class="text-primary" target="_blank" rel="noopener">
|
|
|
|
|
|
политикой обработки персональных данных
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
2026-06-18 23:33:26 +03:00
|
|
|
|
</v-checkbox>
|
2026-06-21 09:34:31 +03:00
|
|
|
|
<SmartCaptchaWidget
|
|
|
|
|
|
ref="captchaRef"
|
|
|
|
|
|
v-model="captchaToken"
|
2026-06-18 23:33:26 +03:00
|
|
|
|
:error-messages="errors.captcha_token"
|
2026-06-21 09:34:31 +03:00
|
|
|
|
/>
|
2026-05-08 17:09:56 +03:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-08 19:59:43 +03:00
|
|
|
|
<v-btn
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
color="primary"
|
|
|
|
|
|
block
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
variant="flat"
|
|
|
|
|
|
:disabled="!canSubmit"
|
|
|
|
|
|
:loading="auth.loading"
|
|
|
|
|
|
>
|
2026-05-08 17:09:56 +03:00
|
|
|
|
Создать аккаунт
|
|
|
|
|
|
</v-btn>
|
|
|
|
|
|
</v-form>
|
|
|
|
|
|
</v-card>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.register-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.register-header h1 {
|
|
|
|
|
|
font-variation-settings: 'opsz' 26;
|
|
|
|
|
|
letter-spacing: -0.018em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.register-form {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.strength-block {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checks {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
2026-05-17 07:45:48 +03:00
|
|
|
|
|
|
|
|
|
|
.password-toggle:focus-visible {
|
|
|
|
|
|
outline: 2px solid currentColor;
|
|
|
|
|
|
outline-offset: 1px;
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
2026-05-08 17:09:56 +03:00
|
|
|
|
</style>
|