logboard/templates/login.html
Сергей Антропов a979dd2838 feat: Добавлена новая система авторизации с JWT токенами
- Удален Basic Auth, заменен на современную JWT авторизацию
- Добавлена страница входа с красивым интерфейсом
- Обновлен фронтенд для работы с JWT токенами
- Добавлены новые зависимости: PyJWT, passlib[bcrypt], jinja2
- Создан тестовый скрипт для проверки авторизации
- Добавлено руководство по миграции
- Обновлена документация и README
- Улучшен дизайн поля ввода пароля на странице входа

Автор: Сергей Антропов
Сайт: https://devops.org.ru
2025-08-17 18:29:06 +03:00

463 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" data-theme="dark">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>LogBoard+ - Вход</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* THEME TOKENS */
:root{
--bg:#0e0f13; --panel:#151821; --muted:#8b94a8; --accent:#7aa2f7; --ok:#9ece6a; --warn:#e0af68; --err:#f7768e; --fg:#e5e9f0;
--border:#2a2f3a; --tab:#1b2030; --tab-active:#22283a; --chip:#2b3142; --link:#9ab8ff;
}
:root[data-theme="light"]{
--bg:#f7f9fc; --panel:#ffffff; --muted:#667085; --accent:#3b82f6; --ok:#15803d; --warn:#b45309; --err:#b91c1c; --fg:#0f172a;
--border:#e5e7eb; --tab:#eef2ff; --tab-active:#dbeafe; --chip:#eef2f7; --link:#1d4ed8;
}
*{box-sizing:border-box}
html,body{height:100%; margin: 0; padding: 0;}
body{background:var(--bg);color:var(--fg);font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;}
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.login-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-logo {
font-size: 32px;
color: var(--accent);
margin-bottom: 16px;
}
.login-title {
font-size: 24px;
font-weight: 600;
color: var(--fg);
margin: 0 0 8px 0;
}
.login-subtitle {
font-size: 14px;
color: var(--muted);
margin: 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-size: 12px;
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-input {
background: var(--chip);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
color: var(--fg);
transition: all 0.2s ease;
font-family: inherit;
}
.form-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(122, 162, 247, 0.1);
}
.form-input::placeholder {
color: var(--muted);
}
.password-input-wrapper {
position: relative;
width: 100%;
}
.password-toggle {
position: absolute;
right: 8px; /* Ближе к краю для всех устройств */
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--muted);
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: all 0.2s ease;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
min-height: 24px;
}
.password-toggle:hover {
color: var(--fg);
background: var(--chip);
}
.password-toggle:active {
transform: translateY(-50%) scale(0.95);
}
.password-input-wrapper .form-input {
padding-right: 40px; /* Место для кнопки */
width: 100%; /* Поле на всю ширину */
}
.login-button {
background: var(--accent);
color: #0b0d12;
border: none;
border-radius: 8px;
padding: 14px 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
margin-top: 8px;
}
.login-button:hover {
background: #6b8fd8;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(122, 162, 247, 0.3);
}
.login-button:disabled {
background: var(--muted);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.error-message {
background: rgba(247, 118, 142, 0.1);
border: 1px solid var(--err);
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
color: var(--err);
display: none;
}
.error-message.show {
display: block;
}
.loading-spinner {
display: none;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
.loading-spinner.show {
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--muted);
}
.footer a {
color: var(--link);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* Theme toggle */
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
}
.theme-toggle input {
appearance: none;
width: 36px;
height: 20px;
border-radius: 999px;
position: relative;
background: var(--chip);
border: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s ease;
}
.theme-toggle input::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--fg);
transition: transform 0.2s ease;
}
.theme-toggle input:checked::after {
transform: translateX(16px);
}
.theme-toggle input:checked {
background: var(--accent);
}
@media (max-width: 480px) {
.login-card {
padding: 24px;
margin: 16px;
}
.login-title {
font-size: 20px;
}
.theme-toggle {
top: 16px;
right: 16px;
padding: 6px 10px;
font-size: 11px;
}
}
</style>
</head>
<body>
<div class="theme-toggle">
<span>Theme</span>
<input id="themeSwitch" type="checkbox" />
</div>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<div class="login-logo">
<i class="fas fa-terminal"></i>
</div>
<h1 class="login-title">LogBoard+</h1>
<p class="login-subtitle">Веб-панель для просмотра логов микросервисов</p>
</div>
<form class="login-form" id="loginForm">
<div class="error-message" id="errorMessage"></div>
<div class="form-group">
<label for="username" class="form-label">Имя пользователя</label>
<input
type="text"
id="username"
name="username"
class="form-input"
placeholder="Введите имя пользователя"
required
autocomplete="username"
>
</div>
<div class="form-group">
<label for="password" class="form-label">Пароль</label>
<div class="password-input-wrapper">
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Введите пароль"
required
autocomplete="current-password"
>
<button type="button" class="password-toggle" id="passwordToggle">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<button type="submit" class="login-button" id="loginButton">
<span class="loading-spinner" id="loadingSpinner"></span>
<span id="buttonText">Войти</span>
</button>
</form>
<div class="footer">
<p>Автор: <a href="https://devops.org.ru" target="_blank">Сергей Антропов</a></p>
</div>
</div>
</div>
<script>
// Theme toggle
(function initTheme(){
const saved = localStorage.lb_theme || 'dark';
document.documentElement.setAttribute('data-theme', saved);
document.getElementById('themeSwitch').checked = (saved==='light');
document.getElementById('themeSwitch').addEventListener('change', ()=>{
const t = document.getElementById('themeSwitch').checked ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', t);
localStorage.lb_theme = t;
});
})();
// Password toggle
document.getElementById('passwordToggle').addEventListener('click', function() {
const passwordInput = document.getElementById('password');
const icon = this.querySelector('i');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
passwordInput.type = 'password';
icon.className = 'fas fa-eye';
}
});
// Login form
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const loginButton = document.getElementById('loginButton');
const buttonText = document.getElementById('buttonText');
const loadingSpinner = document.getElementById('loadingSpinner');
const errorMessage = document.getElementById('errorMessage');
// Validation
if (!username || !password) {
showError('Пожалуйста, заполните все поля');
return;
}
// Show loading state
loginButton.disabled = true;
buttonText.textContent = 'Вход...';
loadingSpinner.classList.add('show');
hideError();
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username,
password: password
})
});
if (response.ok) {
const data = await response.json();
// Store token in localStorage
localStorage.setItem('access_token', data.access_token);
// Redirect to main page
window.location.href = '/';
} else {
const errorData = await response.json();
showError(errorData.detail || 'Ошибка входа в систему');
}
} catch (error) {
console.error('Login error:', error);
showError('Ошибка соединения с сервером');
} finally {
// Reset loading state
loginButton.disabled = false;
buttonText.textContent = 'Войти';
loadingSpinner.classList.remove('show');
}
});
function showError(message) {
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = message;
errorMessage.classList.add('show');
}
function hideError() {
const errorMessage = document.getElementById('errorMessage');
errorMessage.classList.remove('show');
}
// Auto-focus on username field
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('username').focus();
});
// Handle Enter key in password field
document.getElementById('password').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('loginForm').dispatchEvent(new Event('submit'));
}
});
</script>
</body>
</html>