const themeToggle = document.getElementById("theme-toggle"); const themeLabel = document.getElementById("theme-label"); const registrationForm = document.getElementById("registration-form"); const verificationForm = document.getElementById("verification-form"); const verificationStep = document.getElementById("verification-step"); const formStatus = document.getElementById("form-status"); const verificationStatus = document.getElementById("verification-status"); const resendPinButton = document.getElementById("resend-pin"); const countrySelect = document.getElementById("country"); const THEME_KEY = "xcloudx-theme"; let currentSessionId = null; const countries = [ "Россия", "США", "Германия", "Франция", "Испания", "Италия", "Великобритания", "Нидерланды", "Польша", "Швеция", "Норвегия", "Финляндия", "Швейцария", "Австрия", "Канада", "ОАЭ", "Турция", "Казахстан", "Украина", "Беларусь" ]; const validators = { fullName(value) { if (!value.trim()) return "Укажите полное имя"; if (value.trim().length < 3) return "Минимум 3 символа"; if (value.trim().length > 100) return "Максимум 100 символов"; if (!/^[а-яА-ЯёЁa-zA-Z\s'-]+$/.test(value.trim())) return "Допустимы только буквы, пробел, апостроф и дефис"; return ""; }, country(value) { return value ? "" : "Выберите страну"; }, city(value) { if (!value.trim()) return "Укажите город"; if (value.trim().length < 2) return "Минимум 2 символа"; if (value.trim().length > 100) return "Максимум 100 символов"; return ""; }, phoneNumber(value) { const normalized = value.replace(/[^\d+]/g, ""); if (!normalized) return "Укажите номер телефона"; if (!/^\+?[1-9]\d{6,14}$/.test(normalized)) return "Введите номер в международном формате"; return ""; }, email(value) { if (!value.trim()) return "Укажите email"; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim())) return "Некорректный email"; return ""; }, telegramId(value) { if (!value.trim()) return ""; if (!/^(@[a-zA-Z0-9_]{5,32}|https:\/\/t\.me\/[a-zA-Z0-9_]+)$/.test(value.trim())) { return "Введите @username или ссылку https://t.me/username"; } return ""; }, emailPin(value) { if (!value.trim()) return "Введите PIN из email"; if (!/^[A-Z0-9]{4}$/.test(value.trim().toUpperCase())) return "Формат PIN: 4 символа A-Z/0-9"; return ""; }, phonePin(value) { if (!value.trim()) return "Введите PIN из phone"; if (!/^[A-Z0-9]{4}$/.test(value.trim().toUpperCase())) return "Формат PIN: 4 символа A-Z/0-9"; return ""; } }; function populateCountries() { countries.forEach((country) => { const option = document.createElement("option"); option.value = country; option.textContent = country; countrySelect.appendChild(option); }); } function setTheme(theme) { document.documentElement.dataset.theme = theme; localStorage.setItem(THEME_KEY, theme); themeLabel.textContent = theme === "dark" ? "Тёмная тема" : "Светлая тема"; } function initTheme() { const stored = localStorage.getItem(THEME_KEY); if (stored === "dark" || stored === "light") { setTheme(stored); return; } const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; setTheme(prefersDark ? "dark" : "light"); } function setFieldError(name, message) { const target = document.querySelector(`[data-error-for="${name}"]`); if (!target) return; target.textContent = message; target.classList.toggle("has-error", Boolean(message)); } function clearStatus(node) { node.textContent = ""; node.classList.remove("is-success", "is-error"); } function setStatus(node, message, type) { node.textContent = message; node.classList.remove("is-success", "is-error"); if (type) node.classList.add(type); } function getBrowserData() { const userAgent = navigator.userAgent; const language = navigator.language || "unknown"; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown"; const screenResolution = `${window.screen.width}x${window.screen.height}`; const isMobile = /Mobi|Android|iPhone|iPad/i.test(userAgent); const deviceType = /iPad|Tablet/i.test(userAgent) ? "Tablet" : isMobile ? "Mobile" : "Desktop"; return { userAgent, browser: parseBrowser(userAgent), operatingSystem: parseOS(userAgent), language, timezone, screenResolution, isMobile, deviceType }; } function parseBrowser(userAgent) { if (userAgent.includes("Edg/")) return { name: "Edge", version: userAgent.split("Edg/")[1]?.split(" ")[0] || "" }; if (userAgent.includes("YaBrowser/")) return { name: "Yandex Browser", version: userAgent.split("YaBrowser/")[1]?.split(" ")[0] || "" }; if (userAgent.includes("Chrome/")) return { name: "Chrome", version: userAgent.split("Chrome/")[1]?.split(" ")[0] || "" }; if (userAgent.includes("Firefox/")) return { name: "Firefox", version: userAgent.split("Firefox/")[1]?.split(" ")[0] || "" }; if (userAgent.includes("Safari/") && userAgent.includes("Version/")) return { name: "Safari", version: userAgent.split("Version/")[1]?.split(" ")[0] || "" }; return { name: "Unknown", version: "" }; } function parseOS(userAgent) { if (userAgent.includes("Windows NT 10.0")) return { name: "Windows", version: "10/11" }; if (userAgent.includes("Mac OS X")) return { name: "macOS", version: "" }; if (userAgent.includes("Android")) return { name: "Android", version: "" }; if (userAgent.includes("iPhone") || userAgent.includes("iPad")) return { name: "iOS", version: "" }; if (userAgent.includes("Linux")) return { name: "Linux", version: "" }; return { name: "Unknown", version: "" }; } function validateForm(form, fields) { let valid = true; fields.forEach((name) => { const input = form.elements[name]; const message = validators[name](input.value); setFieldError(name, message); if (message) valid = false; }); return valid; } async function submitRegistration(event) { event.preventDefault(); clearStatus(formStatus); const fields = ["fullName", "country", "city", "phoneNumber", "email", "telegramId"]; if (!validateForm(registrationForm, fields)) { setStatus(formStatus, "Исправьте ошибки в форме и повторите попытку.", "is-error"); return; } const payload = { fullName: registrationForm.fullName.value.trim(), country: registrationForm.country.value.trim(), city: registrationForm.city.value.trim(), phoneNumber: registrationForm.phoneNumber.value.trim(), email: registrationForm.email.value.trim(), telegramId: registrationForm.telegramId.value.trim(), browserData: getBrowserData() }; try { setStatus(formStatus, "Отправляем данные на сервер...", ""); const response = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const result = await response.json(); if (!response.ok || !result.success) { setStatus(formStatus, result.error || "Не удалось выполнить регистрацию.", "is-error"); return; } currentSessionId = result.sessionId; verificationStep.classList.remove("is-hidden"); verificationStep.scrollIntoView({ behavior: "smooth", block: "start" }); setStatus(formStatus, result.message, "is-success"); } catch (error) { setStatus(formStatus, "Сервер недоступен. Проверьте backend и повторите попытку.", "is-error"); } } async function submitVerification(event) { event.preventDefault(); clearStatus(verificationStatus); const fields = ["emailPin", "phonePin"]; if (!validateForm(verificationForm, fields)) { setStatus(verificationStatus, "Введите корректные PIN-коды.", "is-error"); return; } if (!currentSessionId) { setStatus(verificationStatus, "Сессия регистрации не найдена. Отправьте форму ещё раз.", "is-error"); return; } try { const response = await fetch("/api/auth/verify-pin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: currentSessionId, emailPin: verificationForm.emailPin.value.trim().toUpperCase(), phonePin: verificationForm.phonePin.value.trim().toUpperCase() }) }); const result = await response.json(); if (!response.ok || !result.success) { const details = result.attemptsRemaining != null ? ` Осталось попыток: ${result.attemptsRemaining}.` : ""; setStatus(verificationStatus, `${result.error || "Ошибка подтверждения."}${details}`, "is-error"); return; } setStatus(verificationStatus, result.message, "is-success"); } catch (error) { setStatus(verificationStatus, "Не удалось проверить PIN-код.", "is-error"); } } async function resendPin() { clearStatus(verificationStatus); if (!currentSessionId) { setStatus(verificationStatus, "Сначала отправьте регистрационную форму.", "is-error"); return; } try { const response = await fetch("/api/auth/resend-pin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: currentSessionId, channel: "email" }) }); const result = await response.json(); if (!response.ok || !result.success) { setStatus(verificationStatus, result.error || "Не удалось переотправить PIN.", "is-error"); return; } setStatus(verificationStatus, result.message, "is-success"); } catch (error) { setStatus(verificationStatus, "Не удалось связаться с сервером.", "is-error"); } } themeToggle.addEventListener("click", () => { setTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark"); }); registrationForm.addEventListener("submit", submitRegistration); verificationForm.addEventListener("submit", submitVerification); resendPinButton.addEventListener("click", resendPin); populateCountries(); initTheme();