/
Rayven2035
/
Web
Обзор
Документация
Войти
/
Rayven2035
/
Web
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
registration.html
636 строк
30 KB
Xusat
registration.html
25 май 2026, 11:20
25 май 2026, 11:20
7786b17
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <title>Логисмарт — Вход в систему</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&display=swap" rel="stylesheet"> <style> * { font-family: 'Inter', sans-serif; } body { background: linear-gradient(135deg, #0f2b3d 0%, #1b4a6e 100%); min-height: 100vh; } .auth-card { background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); transition: all 0.3s ease; } .input-field { transition: all 0.2s ease; border: 1px solid #e2e8f0; } .input-field:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); outline: none; } .input-field.error { border-color: #ef4444; background-color: #fef2f2; } .input-field.success { border-color: #10b981; background-color: #f0fdf4; } .btn-primary { background: linear-gradient(95deg, #2563eb, #1e40af); transition: all 0.2s; } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 20px -5px rgba(37,99,235,0.4); } .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } .toggle-btn { transition: all 0.2s; } .toggle-btn.active { background: #3b82f6; color: white; border-color: #3b82f6; } .password-strength { height: 4px; transition: all 0.3s ease; } </style> </head> <body class="flex items-center justify-center p-4"> <div class="w-full max-w-md"> <div class="text-center mb-8"> <div class="inline-flex items-center justify-center w-20 h-20 bg-white/10 backdrop-blur-md rounded-2xl mb-4 shadow-lg"> <i class="fas fa-route text-white text-4xl"></i> </div> <h1 class="text-3xl font-bold text-white mb-2">Логисмарт</h1> <p class="text-white/70 text-sm">Умная логистика с искусственным интеллектом</p> </div> <div class="auth-card rounded-2xl shadow-2xl overflow-hidden"> <div class="flex border-b border-gray-100"> <button id="loginTabBtn" class="toggle-btn flex-1 py-4 text-center font-semibold transition-all active"> <i class="fas fa-sign-in-alt mr-2"></i> Вход </button> <button id="registerTabBtn" class="toggle-btn flex-1 py-4 text-center font-semibold text-gray-500 transition-all"> <i class="fas fa-user-plus mr-2"></i> Регистрация </button> </div> <!-- Форма входа --> <div id="loginForm" class="p-6 space-y-5"> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-envelope text-gray-400 mr-1"></i> Email </label> <input type="email" id="loginEmail" placeholder="ivan@example.com" autocomplete="email" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="255"> <div id="loginEmailError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-lock text-gray-400 mr-1"></i> Пароль </label> <div class="relative"> <input type="password" id="loginPassword" placeholder="Введите пароль" autocomplete="current-password" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="128"> <button type="button" id="toggleLoginPassword" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"> <i class="fas fa-eye"></i> </button> </div> <div id="loginPasswordError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <div class="flex items-center justify-between"> <label class="flex items-center gap-2 text-sm text-gray-600"> <input type="checkbox" id="rememberMe" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"> <span>Запомнить меня</span> </label> </div> <button id="loginBtn" class="btn-primary w-full text-white py-3 rounded-xl font-semibold"> <i class="fas fa-arrow-right-to-bracket mr-2"></i> Войти </button> <div id="loginAttempts" class="text-center text-xs text-gray-400 hidden"></div> </div> <!-- Форма регистрации --> <div id="registerForm" class="p-6 space-y-4 hidden"> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-user text-gray-400 mr-1"></i> Имя </label> <input type="text" id="regName" placeholder="Иван Иванов" autocomplete="name" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="100"> <div id="regNameError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-envelope text-gray-400 mr-1"></i> Email </label> <input type="email" id="regEmail" placeholder="ivan@example.com" autocomplete="email" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="255"> <div id="regEmailError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-lock text-gray-400 mr-1"></i> Пароль </label> <div class="relative"> <input type="password" id="regPassword" placeholder="Минимум 8 символов" autocomplete="new-password" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="128"> <button type="button" id="toggleRegPassword" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"> <i class="fas fa-eye"></i> </button> </div> <div class="mt-2"> <div class="password-strength w-full bg-gray-200 rounded-full overflow-hidden"> <div id="passwordStrengthBar" class="h-1 w-0 transition-all duration-300"></div> </div> <p id="passwordStrengthText" class="text-xs text-gray-500 mt-1"></p> <div class="flex gap-2 mt-2 text-xs text-gray-500"> <span id="reqLen"><i class="fas fa-circle text-[4px]"></i> 8+ символов</span> <span id="reqUpper"><i class="fas fa-circle text-[4px]"></i> Заглавная</span> <span id="reqLower"><i class="fas fa-circle text-[4px]"></i> Строчная</span> <span id="reqNum"><i class="fas fa-circle text-[4px]"></i> Цифра</span> </div> </div> <div id="regPasswordError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-2"> <i class="fas fa-check-circle text-gray-400 mr-1"></i> Подтвердите пароль </label> <div class="relative"> <input type="password" id="regConfirmPassword" placeholder="Повторите пароль" autocomplete="new-password" class="input-field w-full px-4 py-3 rounded-xl bg-gray-50 focus:bg-white" maxlength="128"> <button type="button" id="toggleRegConfirmPassword" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"> <i class="fas fa-eye"></i> </button> </div> <div id="regConfirmError" class="text-xs text-red-500 mt-1 hidden"></div> </div> <label class="flex items-center gap-2 text-sm text-gray-600"> <input type="checkbox" id="agreeTerms" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"> <span>Я соглашаюсь с <a href="#" class="text-blue-600">условиями использования</a></span> </label> <button id="registerBtn" class="btn-primary w-full text-white py-3 rounded-xl font-semibold"> <i class="fas fa-user-plus mr-2"></i> Зарегистрироваться </button> <p class="text-center text-xs text-gray-500"> Регистрируясь, вы подтверждаете, что вам есть 18 лет </p> </div> </div> <div class="text-center mt-6 text-white/50 text-xs"> <p>© 2026 Логисмарт — AI-диспетчер для грузоперевозок</p> <p class="mt-1">Защищено шифрованием 256-bit | HTTPS</p> </div> </div> <div id="toastContainer" class="fixed bottom-4 right-4 z-50 space-y-2"></div> <script> // ========== SECURITY CONFIG ========== const API_BASE_URL = ''; let loginAttempts = 0; let lockoutUntil = null; const MAX_LOGIN_ATTEMPTS = 5; const LOCKOUT_DURATION = 5 * 60 * 1000; // 5 минут // ========== XSS PROTECTION ========== function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function sanitizeInput(str) { if (typeof str !== 'string') return ''; return str .replace(/[<>'"&]/g, '') .trim() .slice(0, 255); } // ========== API FUNCTIONS ========== async function apiRegister(name, email, password) { const response = await fetch(`${API_BASE_URL}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, password }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Registration failed'); } return await response.json(); } async function apiLogin(email, password) { const formData = new FormData(); formData.append('username', email); formData.append('password', password); const response = await fetch(`${API_BASE_URL}/token`, { method: 'POST', body: formData }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Login failed'); } return await response.json(); } // ========== SESSION MANAGEMENT ========== function checkSession() { const token = localStorage.getItem('logismart_token'); if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); if (payload.exp * 1000 > Date.now()) { return true; } } catch(e) {} } localStorage.removeItem('logismart_token'); localStorage.removeItem('logismart_user'); return false; } function createSession(token, user, remember = false) { localStorage.setItem('logismart_token', token); localStorage.setItem('logismart_user', JSON.stringify(user)); } function redirectToApp() { localStorage.setItem('logismart_auth', 'true'); showToast('Успешный вход! Перенаправление...', 'success'); setTimeout(() => { window.location.href = 'Web-приложение.html'; }, 1000); } // ========== VALIDATION ========== function validateEmail(email) { const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return re.test(email) && email.length <= 255; } function validateName(name) { const re = /^[а-яёА-ЯЁa-zA-Z\s'-]+$/; return re.test(name) && name.length >= 2 && name.length <= 100; } function validatePassword(password) { return { valid: password.length >= 8 && /[A-Z]/.test(password) && /[a-z]/.test(password) && /[0-9]/.test(password), checks: { length: password.length >= 8, upper: /[A-Z]/.test(password), lower: /[a-z]/.test(password), number: /[0-9]/.test(password) } }; } function updatePasswordStrength(password) { const bar = document.getElementById('passwordStrengthBar'); const text = document.getElementById('passwordStrengthText'); if (!bar || !text) return; const result = validatePassword(password); let strength = 0; if (result.checks.length) strength++; if (result.checks.upper) strength++; if (result.checks.lower) strength++; if (result.checks.number) strength++; const colors = ['#ef4444', '#f59e0b', '#eab308', '#3b82f6', '#10b981']; const widths = ['0%', '25%', '50%', '75%', '100%']; const labels = ['', 'Очень слабый', 'Слабый', 'Средний', 'Хороший', 'Отличный']; bar.style.width = widths[strength]; bar.style.backgroundColor = colors[strength]; text.textContent = password ? `Надёжность: ${labels[strength]}` : ''; document.getElementById('reqLen').className = result.checks.length ? 'text-green-600' : 'text-gray-500'; document.getElementById('reqLen').innerHTML = result.checks.length ? '<i class="fas fa-check text-green-600"></i> 8+ символов' : '<i class="fas fa-circle text-[4px]"></i> 8+ символов'; document.getElementById('reqUpper').className = result.checks.upper ? 'text-green-600' : 'text-gray-500'; document.getElementById('reqUpper').innerHTML = result.checks.upper ? '<i class="fas fa-check text-green-600"></i> Заглавная' : '<i class="fas fa-circle text-[4px]"></i> Заглавная'; document.getElementById('reqLower').className = result.checks.lower ? 'text-green-600' : 'text-gray-500'; document.getElementById('reqLower').innerHTML = result.checks.lower ? '<i class="fas fa-check text-green-600"></i> Строчная' : '<i class="fas fa-circle text-[4px]"></i> Строчная'; document.getElementById('reqNum').className = result.checks.number ? 'text-green-600' : 'text-gray-500'; document.getElementById('reqNum').innerHTML = result.checks.number ? '<i class="fas fa-check text-green-600"></i> Цифра' : '<i class="fas fa-circle text-[4px]"></i> Цифра'; return result.valid; } function showError(fieldId, message) { const errorEl = document.getElementById(`${fieldId}Error`); const inputEl = document.getElementById(fieldId); if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); } if (inputEl) { inputEl.classList.add('error'); inputEl.classList.remove('success'); } } function clearError(fieldId) { const errorEl = document.getElementById(`${fieldId}Error`); const inputEl = document.getElementById(fieldId); if (errorEl) { errorEl.classList.add('hidden'); errorEl.textContent = ''; } if (inputEl) { inputEl.classList.remove('error'); } } function showSuccess(fieldId) { const inputEl = document.getElementById(fieldId); if (inputEl) { inputEl.classList.add('success'); inputEl.classList.remove('error'); } } // ========== TOAST NOTIFICATIONS ========== function showToast(message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); const icons = { success: '<i class="fas fa-check-circle"></i>', error: '<i class="fas fa-exclamation-circle"></i>', warning: '<i class="fas fa-triangle-exclamation"></i>', info: '<i class="fas fa-info-circle"></i>' }; const bgColors = { success: 'bg-green-500', error: 'bg-red-500', warning: 'bg-amber-500', info: 'bg-blue-500' }; toast.className = `${bgColors[type]} text-white px-5 py-3 rounded-xl shadow-lg flex items-center gap-2 text-sm transform transition-all duration-300 translate-x-full`; toast.innerHTML = `${icons[type]} ${escapeHtml(message)}`; container.appendChild(toast); setTimeout(() => { toast.classList.remove('translate-x-full'); toast.classList.add('translate-x-0'); }, 10); setTimeout(() => { toast.classList.remove('translate-x-0'); toast.classList.add('translate-x-full'); setTimeout(() => toast.remove(), 300); }, 3000); } // ========== FORM HANDLERS ========== async function handleLogin() { // Check lockout if (lockoutUntil && Date.now() < lockoutUntil) { const remaining = Math.ceil((lockoutUntil - Date.now()) / 1000 / 60); showToast(`Слишком много попыток. Попробуйте через ${remaining} мин`, 'warning'); return; } const email = sanitizeInput(document.getElementById('loginEmail').value.trim()); const password = document.getElementById('loginPassword').value; const remember = document.getElementById('rememberMe').checked; // Clear errors clearError('loginEmail'); clearError('loginPassword'); let hasError = false; if (!email) { showError('loginEmail', 'Введите email'); hasError = true; } else if (!validateEmail(email)) { showError('loginEmail', 'Некорректный формат email'); hasError = true; } else { showSuccess('loginEmail'); } if (!password) { showError('loginPassword', 'Введите пароль'); hasError = true; } if (hasError) return; try { const data = await apiLogin(email, password); const user = { id: 0, name: email, email: email }; createSession(data.access_token, user, remember); loginAttempts = 0; localStorage.removeItem('loginAttempts'); showToast('Добро пожаловать!', 'success'); redirectToApp(); } catch (error) { loginAttempts++; localStorage.setItem('loginAttempts', loginAttempts); if (loginAttempts >= MAX_LOGIN_ATTEMPTS) { lockoutUntil = Date.now() + LOCKOUT_DURATION; localStorage.setItem('lockoutUntil', lockoutUntil); showToast('Слишком много попыток. Вход заблокирован на 5 минут', 'error'); } else { showToast(error.message || 'Неверный email или пароль', 'error'); document.getElementById('loginPassword').classList.add('error'); const attemptsLeft = MAX_LOGIN_ATTEMPTS - loginAttempts; const attemptsEl = document.getElementById('loginAttempts'); attemptsEl.textContent = `Осталось попыток: ${attemptsLeft}`; attemptsEl.classList.remove('hidden'); } } } async function handleRegister() { const name = sanitizeInput(document.getElementById('regName').value.trim()); const email = sanitizeInput(document.getElementById('regEmail').value.trim().toLowerCase()); const password = document.getElementById('regPassword').value; const confirmPassword = document.getElementById('regConfirmPassword').value; const agreeTerms = document.getElementById('agreeTerms').checked; // Clear errors ['regName', 'regEmail', 'regPassword', 'regConfirm'].forEach(clearError); let hasError = false; if (!name) { showError('regName', 'Введите имя'); hasError = true; } else if (!validateName(name)) { showError('regName', 'Имя: только буквы, 2-100 символов'); hasError = true; } else { showSuccess('regName'); } if (!email) { showError('regEmail', 'Введите email'); hasError = true; } else if (!validateEmail(email)) { showError('regEmail', 'Некорректный формат email'); hasError = true; } else { showSuccess('regEmail'); } const passwordResult = validatePassword(password); if (!password) { showError('regPassword', 'Введите пароль'); hasError = true; } else if (!passwordResult.valid) { showError('regPassword', 'Пароль слишком слабый'); hasError = true; } else { showSuccess('regPassword'); } if (!confirmPassword) { showError('regConfirm', 'Подтвердите пароль'); hasError = true; } else if (password !== confirmPassword) { showError('regConfirm', 'Пароли не совпадают'); hasError = true; } else { showSuccess('regConfirm'); } if (!agreeTerms) { showToast('Примите условия использования', 'warning'); hasError = true; } if (hasError) return; try { await apiRegister(name, email, password); showToast('Регистрация успешна! Войдите в систему', 'success'); switchToLogin(); document.getElementById('regName').value = ''; document.getElementById('regEmail').value = ''; document.getElementById('regPassword').value = ''; document.getElementById('regConfirmPassword').value = ''; document.getElementById('agreeTerms').checked = false; updatePasswordStrength(''); } catch (error) { showToast(error.message || 'Ошибка регистрации', 'error'); } } // ========== UI HELPERS ========== function switchToLogin() { document.getElementById('loginForm').classList.remove('hidden'); document.getElementById('registerForm').classList.add('hidden'); document.getElementById('loginTabBtn').classList.add('active', 'bg-blue-600', 'text-white'); document.getElementById('loginTabBtn').classList.remove('text-gray-500'); document.getElementById('registerTabBtn').classList.remove('active', 'bg-blue-600', 'text-white'); document.getElementById('registerTabBtn').classList.add('text-gray-500'); } function switchToRegister() { document.getElementById('loginForm').classList.add('hidden'); document.getElementById('registerForm').classList.remove('hidden'); document.getElementById('registerTabBtn').classList.add('active', 'bg-blue-600', 'text-white'); document.getElementById('registerTabBtn').classList.remove('text-gray-500'); document.getElementById('loginTabBtn').classList.remove('active', 'bg-blue-600', 'text-white'); document.getElementById('loginTabBtn').classList.add('text-gray-500'); } function togglePassword(inputId, btnId) { const input = document.getElementById(inputId); const btn = document.getElementById(btnId); if (input.type === 'password') { input.type = 'text'; btn.innerHTML = '<i class="fas fa-eye-slash"></i>'; } else { input.type = 'password'; btn.innerHTML = '<i class="fas fa-eye"></i>'; } } // ========== INITIALIZATION ========== function init() { // Check session if (checkSession()) { window.location.href = 'Web-приложение.html'; return; } // Restore login attempts loginAttempts = parseInt(localStorage.getItem('loginAttempts') || '0'); lockoutUntil = parseInt(localStorage.getItem('lockoutUntil') || '0'); if (lockoutUntil && Date.now() < lockoutUntil) { const remaining = Math.ceil((lockoutUntil - Date.now()) / 1000 / 60); showToast(`Вход заблокирован. Попробуйте через ${remaining} мин`, 'warning'); } else if (loginAttempts > 0) { const attemptsEl = document.getElementById('loginAttempts'); const attemptsLeft = MAX_LOGIN_ATTEMPTS - loginAttempts; attemptsEl.textContent = `Осталось попыток: ${attemptsLeft}`; attemptsEl.classList.remove('hidden'); } // Event listeners document.getElementById('loginBtn').addEventListener('click', handleLogin); document.getElementById('registerBtn').addEventListener('click', handleRegister); document.getElementById('loginTabBtn').addEventListener('click', switchToLogin); document.getElementById('registerTabBtn').addEventListener('click', switchToRegister); document.getElementById('toggleLoginPassword').addEventListener('click', () => togglePassword('loginPassword', 'toggleLoginPassword')); document.getElementById('toggleRegPassword').addEventListener('click', () => togglePassword('regPassword', 'toggleRegPassword')); document.getElementById('toggleRegConfirmPassword').addEventListener('click', () => togglePassword('regConfirmPassword', 'toggleRegConfirmPassword')); document.getElementById('regPassword').addEventListener('input', (e) => { updatePasswordStrength(e.target.value); }); // Enter key handlers document.getElementById('loginPassword').addEventListener('keypress', (e) => { if (e.key === 'Enter') handleLogin(); }); document.getElementById('loginEmail').addEventListener('keypress', (e) => { if (e.key === 'Enter') handleLogin(); }); // Real-time validation document.getElementById('regName').addEventListener('blur', (e) => { const value = sanitizeInput(e.target.value.trim()); if (value && validateName(value)) { showSuccess('regName'); } else if (value) { showError('regName', 'Имя: только буквы, 2-100 символов'); } }); document.getElementById('regEmail').addEventListener('blur', (e) => { const value = sanitizeInput(e.target.value.trim().toLowerCase()); if (value && validateEmail(value)) { showSuccess('regEmail'); } else if (value) { showError('regEmail', 'Некорректный формат email'); } }); } init(); </script> </body> </html>