2026-03-30 20:21:41 +05:00
|
|
|
|
---
|
|
|
|
|
|
import Button from "../base/Button.astro";
|
2026-05-03 15:57:35 +05:00
|
|
|
|
import ConsentCheckbox from "../base/ConsentCheckbox.astro";
|
2026-03-30 20:21:41 +05:00
|
|
|
|
|
|
|
|
|
|
// Иконки
|
|
|
|
|
|
const emailIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`;
|
|
|
|
|
|
|
|
|
|
|
|
const lockIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>`;
|
|
|
|
|
|
|
|
|
|
|
|
const checkIcon = `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 13l4 4L19 7"/></svg>`;
|
|
|
|
|
|
|
|
|
|
|
|
const arrowLeftIcon = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>`;
|
|
|
|
|
|
|
|
|
|
|
|
// Props
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
// Режим: 'request' - запрос сброса, 'reset' - установка нового пароля
|
|
|
|
|
|
mode?: "request" | "reset";
|
|
|
|
|
|
token?: string; // Токен из URL для режима reset
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { mode = "request", token = "" } = Astro.props;
|
|
|
|
|
|
|
|
|
|
|
|
const isResetMode = mode === "reset";
|
|
|
|
|
|
const title = isResetMode ? "Новый пароль" : "Восстановление пароля";
|
|
|
|
|
|
const subtitle = isResetMode
|
|
|
|
|
|
? "Придумайте новый пароль для вашего аккаунта"
|
|
|
|
|
|
: "Введите email, и мы отправим вам ссылку для сброса пароля";
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
<div class="w-full max-w-md mx-auto">
|
|
|
|
|
|
<!-- Назад к входу -->
|
|
|
|
|
|
<a
|
|
|
|
|
|
href="/auth/login"
|
|
|
|
|
|
class="inline-flex items-center gap-2 text-sm text-gray-500 hover:text-gold transition-colors mb-8"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Fragment set:html={arrowLeftIcon} />
|
|
|
|
|
|
<span>Вернуться к входу</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Заголовок -->
|
|
|
|
|
|
<div class="text-center mb-8">
|
|
|
|
|
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
|
|
|
|
|
|
<p class="text-sm text-gray-500">{subtitle}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Форма запроса сброса -->
|
|
|
|
|
|
{!isResetMode && (
|
|
|
|
|
|
<form class="space-y-6" id="forgot-form" novalidate>
|
|
|
|
|
|
<!-- Honeypot -->
|
|
|
|
|
|
<div class="honeypot-field" aria-hidden="true">
|
|
|
|
|
|
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Email -->
|
|
|
|
|
|
<div class="relative group">
|
|
|
|
|
|
<label for="email" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
|
|
|
|
|
|
Email <span class="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div class="relative">
|
|
|
|
|
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
|
|
|
|
<Fragment set:html={emailIcon} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
id="email"
|
|
|
|
|
|
name="email"
|
|
|
|
|
|
placeholder="you@example.com"
|
|
|
|
|
|
required
|
|
|
|
|
|
maxlength="254"
|
|
|
|
|
|
pattern="[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
|
|
class="w-full pl-12 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="error-message hidden text-red-500 text-xs mt-1">
|
|
|
|
|
|
Введите корректный email адрес
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Submit -->
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
text="Отправить ссылку"
|
|
|
|
|
|
variant="primary-white-text"
|
|
|
|
|
|
size="md"
|
|
|
|
|
|
className="w-full!"
|
|
|
|
|
|
id="submit-btn"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
/>
|
2026-05-03 15:57:35 +05:00
|
|
|
|
<ConsentCheckbox formId="forgot-form" />
|
2026-03-30 20:21:41 +05:00
|
|
|
|
</form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Форма установки нового пароля -->
|
|
|
|
|
|
{isResetMode && (
|
|
|
|
|
|
<form class="space-y-6" id="reset-form" novalidate data-token={token}>
|
|
|
|
|
|
<!-- Honeypot -->
|
|
|
|
|
|
<div class="honeypot-field" aria-hidden="true">
|
|
|
|
|
|
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Новый пароль -->
|
|
|
|
|
|
<div class="relative group">
|
|
|
|
|
|
<label for="password" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
|
|
|
|
|
|
Новый пароль <span class="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div class="relative">
|
|
|
|
|
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
|
|
|
|
<Fragment set:html={lockIcon} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
id="password"
|
|
|
|
|
|
name="password"
|
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
|
required
|
|
|
|
|
|
minlength="8"
|
|
|
|
|
|
maxlength="12"
|
|
|
|
|
|
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
id="toggle-password"
|
|
|
|
|
|
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
|
|
|
|
|
|
aria-label="Показать пароль"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<svg id="eye-off-icon" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="error-message hidden text-red-500 text-xs mt-1">
|
|
|
|
|
|
Пароль должен содержать хотя бы одну букву и одну цифру
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<p class="text-xs text-gray-400 mt-1">От 8 до 12 символов, буква + цифра</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Подтверждение пароля -->
|
|
|
|
|
|
<div class="relative group">
|
|
|
|
|
|
<label for="confirm-password" class="block text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
|
|
|
|
|
|
Повторите пароль <span class="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div class="relative">
|
|
|
|
|
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
|
|
|
|
<Fragment set:html={checkIcon} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
id="confirm-password"
|
|
|
|
|
|
name="confirmPassword"
|
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
|
required
|
|
|
|
|
|
minlength="8"
|
|
|
|
|
|
maxlength="12"
|
|
|
|
|
|
class="w-full pl-12 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl text-gray-900 font-medium focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
id="toggle-confirm-password"
|
|
|
|
|
|
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 transition-colors"
|
|
|
|
|
|
aria-label="Показать пароль"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg id="eye-icon-confirm" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<svg id="eye-off-icon-confirm" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="error-message hidden text-red-500 text-xs mt-1">
|
|
|
|
|
|
Пароли не совпадают
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Submit -->
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
text="Сохранить пароль"
|
|
|
|
|
|
variant="primary-white-text"
|
|
|
|
|
|
size="md"
|
|
|
|
|
|
className="w-full!"
|
|
|
|
|
|
id="submit-btn"
|
|
|
|
|
|
disabled
|
|
|
|
|
|
/>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.honeypot-field {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -9999px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// Определяем режим по наличию формы
|
|
|
|
|
|
const isResetMode = !!document.getElementById('reset-form');
|
|
|
|
|
|
const form = document.getElementById(isResetMode ? 'reset-form' : 'forgot-form');
|
|
|
|
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
|
|
|
|
const honeypotField = document.getElementById('website');
|
|
|
|
|
|
|
|
|
|
|
|
// Логирование
|
|
|
|
|
|
function log(message, data) {
|
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
console.log(`[PASSWORD_RESET_FORM][${timestamp}] ${message}`, data || '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function logError(message, data) {
|
|
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
|
|
console.error(`[PASSWORD_RESET_FORM][${timestamp}] ERROR: ${message}`, data || '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const touchedFields = new Set();
|
|
|
|
|
|
|
|
|
|
|
|
// Утилита для ошибок
|
|
|
|
|
|
function showFieldError(input, show, message = "") {
|
|
|
|
|
|
const group = input.closest('.group');
|
|
|
|
|
|
const errorEl = group?.querySelector('.error-message');
|
|
|
|
|
|
|
|
|
|
|
|
if (errorEl) {
|
|
|
|
|
|
if (show) {
|
|
|
|
|
|
if (message) errorEl.textContent = message;
|
|
|
|
|
|
errorEl.classList.remove('hidden');
|
|
|
|
|
|
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
|
|
|
|
|
|
input.classList.remove('focus:border-gold', 'focus:ring-gold/20');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errorEl.classList.add('hidden');
|
|
|
|
|
|
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500/20');
|
|
|
|
|
|
input.classList.add('focus:border-gold', 'focus:ring-gold/20');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Валидация email
|
|
|
|
|
|
function validateEmail(showError = false) {
|
|
|
|
|
|
const emailInput = document.getElementById('email');
|
|
|
|
|
|
if (!emailInput) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const value = emailInput.value.trim();
|
|
|
|
|
|
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
|
|
|
|
let isValid = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (!value || !emailPattern.test(value) || value.length > 254) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showError || touchedFields.has('email')) {
|
|
|
|
|
|
showFieldError(emailInput, !isValid && value.length > 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return isValid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Валидация пароля
|
|
|
|
|
|
function validatePassword(showError = false) {
|
|
|
|
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
|
|
if (!passwordInput) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const value = passwordInput.value;
|
|
|
|
|
|
const minLength = 8;
|
|
|
|
|
|
const maxLength = 12;
|
|
|
|
|
|
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+$/;
|
|
|
|
|
|
let isValid = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (!value || value.length < minLength || value.length > maxLength) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
} else if (!passwordRegex.test(value)) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showError || touchedFields.has('password')) {
|
|
|
|
|
|
showFieldError(passwordInput, !isValid && value.length > 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return isValid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Валидация подтверждения пароля
|
|
|
|
|
|
function validateConfirmPassword(showError = false) {
|
|
|
|
|
|
const confirmInput = document.getElementById('confirm-password');
|
|
|
|
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
|
|
if (!confirmInput || !passwordInput) return true;
|
|
|
|
|
|
|
|
|
|
|
|
const value = confirmInput.value;
|
|
|
|
|
|
const passwordValue = passwordInput.value;
|
|
|
|
|
|
let isValid = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (!value || value !== passwordValue) {
|
|
|
|
|
|
isValid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showError || touchedFields.has('confirmPassword')) {
|
|
|
|
|
|
showFieldError(confirmInput, !isValid && value.length > 0,
|
|
|
|
|
|
value !== passwordValue ? 'Пароли не совпадают' : '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return isValid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Проверка всей формы
|
|
|
|
|
|
function checkFormValidity() {
|
|
|
|
|
|
const isHoneypotEmpty = !honeypotField?.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (isResetMode) {
|
|
|
|
|
|
const isPasswordValid = validatePassword(false);
|
|
|
|
|
|
const isConfirmValid = validateConfirmPassword(false);
|
|
|
|
|
|
submitBtn.disabled = !(isPasswordValid && isConfirmValid && isHoneypotEmpty);
|
|
|
|
|
|
return isPasswordValid && isConfirmValid && isHoneypotEmpty;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const isEmailValid = validateEmail(false);
|
|
|
|
|
|
submitBtn.disabled = !(isEmailValid && isHoneypotEmpty);
|
|
|
|
|
|
return isEmailValid && isHoneypotEmpty;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Показать все ошибки
|
|
|
|
|
|
function showAllErrors() {
|
|
|
|
|
|
if (isResetMode) {
|
|
|
|
|
|
touchedFields.add('password');
|
|
|
|
|
|
touchedFields.add('confirmPassword');
|
|
|
|
|
|
const isPasswordValid = validatePassword(true);
|
|
|
|
|
|
const isConfirmValid = validateConfirmPassword(true);
|
|
|
|
|
|
return isPasswordValid && isConfirmValid;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
touchedFields.add('email');
|
|
|
|
|
|
return validateEmail(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Настройка переключателя пароля
|
|
|
|
|
|
function setupPasswordToggle(btnId, inputId, eyeId, eyeOffId) {
|
|
|
|
|
|
const btn = document.getElementById(btnId);
|
|
|
|
|
|
const input = document.getElementById(inputId);
|
|
|
|
|
|
const eyeIcon = document.getElementById(eyeId);
|
|
|
|
|
|
const eyeOffIcon = document.getElementById(eyeOffId);
|
|
|
|
|
|
|
|
|
|
|
|
btn?.addEventListener('click', () => {
|
|
|
|
|
|
const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
|
|
|
|
input.setAttribute('type', type);
|
|
|
|
|
|
|
|
|
|
|
|
if (type === 'text') {
|
|
|
|
|
|
eyeIcon?.classList.add('hidden');
|
|
|
|
|
|
eyeOffIcon?.classList.remove('hidden');
|
|
|
|
|
|
btn.setAttribute('aria-label', 'Скрыть пароль');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
eyeIcon?.classList.remove('hidden');
|
|
|
|
|
|
eyeOffIcon?.classList.add('hidden');
|
|
|
|
|
|
btn.setAttribute('aria-label', 'Показать пароль');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Режим запроса сброса
|
|
|
|
|
|
if (!isResetMode) {
|
|
|
|
|
|
const emailInput = document.getElementById('email');
|
|
|
|
|
|
|
|
|
|
|
|
// Ограничение ввода email
|
|
|
|
|
|
emailInput?.addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (!/[a-zA-Z0-9@._%+\-]/.test(e.key) &&
|
|
|
|
|
|
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
emailInput?.addEventListener('input', (e) => {
|
|
|
|
|
|
e.target.value = e.target.value.replace(/\s/g, '');
|
|
|
|
|
|
validateEmail();
|
|
|
|
|
|
checkFormValidity();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
emailInput?.addEventListener('focus', () => touchedFields.add('email'));
|
|
|
|
|
|
emailInput?.addEventListener('blur', () => {
|
|
|
|
|
|
touchedFields.add('email');
|
|
|
|
|
|
if (emailInput.value.length > 0) validateEmail(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Отправка формы запроса
|
|
|
|
|
|
form?.addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
if (honeypotField?.value) {
|
|
|
|
|
|
console.log('Bot detected');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!showAllErrors()) return;
|
|
|
|
|
|
|
|
|
|
|
|
submitBtn.disabled = true;
|
|
|
|
|
|
const originalText = submitBtn.textContent;
|
|
|
|
|
|
submitBtn.textContent = 'Отправка...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
|
const email = formData.get('email');
|
|
|
|
|
|
|
|
|
|
|
|
log(`Запрос сброса пароля для: ${email}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Редирект на страницу успеха с email
|
|
|
|
|
|
log('Перенаправление на страницу подтверждения отправки');
|
|
|
|
|
|
window.location.href = `/auth/forgot-password-sent?email=${encodeURIComponent(email)}`;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logError('❌ Ошибка сброса пароля:', error);
|
|
|
|
|
|
alert(error.message || 'Ошибка отправки. Попробуйте позже.');
|
|
|
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
|
submitBtn.textContent = originalText;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Режим установки нового пароля
|
|
|
|
|
|
if (isResetMode) {
|
|
|
|
|
|
const passwordInput = document.getElementById('password');
|
|
|
|
|
|
const confirmInput = document.getElementById('confirm-password');
|
|
|
|
|
|
const token = form?.dataset.token;
|
|
|
|
|
|
|
|
|
|
|
|
// Переключатели видимости пароля
|
|
|
|
|
|
setupPasswordToggle('toggle-password', 'password', 'eye-icon', 'eye-off-icon');
|
|
|
|
|
|
setupPasswordToggle('toggle-confirm-password', 'confirm-password', 'eye-icon-confirm', 'eye-off-icon-confirm');
|
|
|
|
|
|
|
|
|
|
|
|
// Ограничение ввода пароля
|
|
|
|
|
|
passwordInput?.addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (passwordInput.value.length >= 12 &&
|
|
|
|
|
|
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
passwordInput?.addEventListener('input', (e) => {
|
|
|
|
|
|
if (e.target.value.length > 12) {
|
|
|
|
|
|
e.target.value = e.target.value.slice(0, 12);
|
|
|
|
|
|
}
|
|
|
|
|
|
e.target.value = e.target.value.replace(/\s/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
validatePassword();
|
|
|
|
|
|
if (confirmInput.value) validateConfirmPassword();
|
|
|
|
|
|
checkFormValidity();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
confirmInput?.addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (confirmInput.value.length >= 12 &&
|
|
|
|
|
|
!['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.key)) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
confirmInput?.addEventListener('input', (e) => {
|
|
|
|
|
|
if (e.target.value.length > 12) {
|
|
|
|
|
|
e.target.value = e.target.value.slice(0, 12);
|
|
|
|
|
|
}
|
|
|
|
|
|
e.target.value = e.target.value.replace(/\s/g, '');
|
|
|
|
|
|
|
|
|
|
|
|
validateConfirmPassword();
|
|
|
|
|
|
checkFormValidity();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Фокус и blur
|
|
|
|
|
|
passwordInput?.addEventListener('focus', () => touchedFields.add('password'));
|
|
|
|
|
|
confirmInput?.addEventListener('focus', () => touchedFields.add('confirmPassword'));
|
|
|
|
|
|
|
|
|
|
|
|
passwordInput?.addEventListener('blur', () => {
|
|
|
|
|
|
touchedFields.add('password');
|
|
|
|
|
|
if (passwordInput.value.length > 0) validatePassword(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
confirmInput?.addEventListener('blur', () => {
|
|
|
|
|
|
touchedFields.add('confirmPassword');
|
|
|
|
|
|
if (confirmInput.value.length > 0) validateConfirmPassword(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Отправка формы сброса
|
|
|
|
|
|
form?.addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
if (honeypotField?.value) {
|
|
|
|
|
|
console.log('Bot detected');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!showAllErrors()) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
alert('Ошибка: отсутствует токен сброса');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
submitBtn.disabled = true;
|
|
|
|
|
|
const originalText = submitBtn.textContent;
|
|
|
|
|
|
submitBtn.textContent = 'Сохранение...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
|
const password = formData.get('password');
|
|
|
|
|
|
const passwordConfirm = formData.get('confirmPassword');
|
|
|
|
|
|
|
|
|
|
|
|
log(`Сброс пароля с токеном: ${token}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Сохраняем данные в sessionStorage для страницы подтверждения
|
|
|
|
|
|
sessionStorage.setItem('passwordResetData', JSON.stringify({
|
|
|
|
|
|
password,
|
|
|
|
|
|
passwordConfirm
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// Редирект на страницу подтверждения с токеном
|
|
|
|
|
|
log('Перенаправление на страницу подтверждения');
|
|
|
|
|
|
window.location.href = `/auth/password-reset-success?token=${encodeURIComponent(token)}`;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
logError('❌ Ошибка сброса пароля:', error);
|
|
|
|
|
|
// Очищаем sessionStorage при ошибке
|
|
|
|
|
|
sessionStorage.removeItem('passwordResetData');
|
|
|
|
|
|
alert(error.message || 'Ошибка сброса пароля. Возможно, ссылка устарела.');
|
|
|
|
|
|
submitBtn.disabled = false;
|
|
|
|
|
|
submitBtn.textContent = originalText;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Начальная проверка
|
|
|
|
|
|
checkFormValidity();
|
|
|
|
|
|
</script>
|