feat: Добавлена новая система авторизации с JWT токенами

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

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов
2025-08-17 18:29:06 +03:00
parent 3126ff4eb6
commit a979dd2838
10 changed files with 1238 additions and 98 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>LogBoard+</title>
<meta name="x-token" content="__TOKEN__"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* THEME TOKENS */
@@ -1336,7 +1336,6 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
console.log('LogBoard+ script loaded - VERSION 2');
const state = {
token: (document.querySelector('meta[name="x-token"]')?.content)||'',
services: [],
current: null,
open: {}, // id -> {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName}
@@ -1714,8 +1713,24 @@ async function fetchProjects(){
try {
console.log('Fetching projects...');
const url = new URL(location.origin + '/api/projects');
const res = await fetch(url);
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
window.location.href = '/login';
return;
}
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok){
if (res.status === 401) {
console.error('Unauthorized, redirecting to login');
window.location.href = '/login';
return;
}
console.error('Failed to fetch projects:', res.status, res.statusText);
return;
}
@@ -1835,8 +1850,23 @@ function getSelectedProjects() {
// Функции для работы с исключенными контейнерами
async function loadExcludedContainers() {
try {
const response = await fetch('/api/excluded-containers');
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
return [];
}
const response = await fetch('/api/excluded-containers', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
if (response.status === 401) {
console.error('Unauthorized, redirecting to login');
window.location.href = '/login';
return [];
}
console.error('Ошибка загрузки исключенных контейнеров:', response.status);
return [];
}
@@ -1850,15 +1880,27 @@ async function loadExcludedContainers() {
async function saveExcludedContainers(containers) {
try {
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
return false;
}
const response = await fetch('/api/excluded-containers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(containers)
});
if (!response.ok) {
if (response.status === 401) {
console.error('Unauthorized, redirecting to login');
window.location.href = '/login';
return false;
}
console.error('Ошибка сохранения исключенных контейнеров:', response.status);
return false;
}
@@ -2381,8 +2423,24 @@ async function fetchServices(){
url.searchParams.set('projects', selectedProjects.join(','));
}
const res = await fetch(url);
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
window.location.href = '/login';
return;
}
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok){
if (res.status === 401) {
console.error('Unauthorized, redirecting to login');
window.location.href = '/login';
return;
}
console.error('Auth failed (HTTP):', res.status, res.statusText);
alert('Auth failed (HTTP)');
return;
@@ -2404,7 +2462,7 @@ async function fetchServices(){
function wsUrl(containerId, service, project){
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const tail = els.tail.value || '500';
const token = encodeURIComponent(state.token || '');
const token = encodeURIComponent(localStorage.getItem('access_token') || '');
const sp = service?`&service=${encodeURIComponent(service)}`:'';
const pj = project?`&project=${encodeURIComponent(project)}`:'';
if (els.aggregate && els.aggregate.checked && service){
@@ -2441,9 +2499,28 @@ async function sendSnapshot(id){
console.log('Saving snapshot with content length:', text.length);
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
window.location.href = '/login';
return;
}
const payload = {container_id: id, service: o.serviceName || id, content: text};
const res = await fetch('/api/snapshot', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
const res = await fetch('/api/snapshot', {
method:'POST',
headers:{
'Content-Type':'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!res.ok){
if (res.status === 401) {
console.error('Unauthorized, redirecting to login');
window.location.href = '/login';
return;
}
console.error('Snapshot failed:', res.status, res.statusText);
alert('snapshot failed');
return;
@@ -3545,7 +3622,7 @@ els.copyFab.addEventListener('click', async ()=>{
function fanGroupUrl(servicesCsv, project){
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const tail = els.tail.value || '500';
const token = encodeURIComponent(state.token || '');
const token = encodeURIComponent(localStorage.getItem('access_token') || '');
const pj = project?`&project=${encodeURIComponent(project)}`:'';
return `${proto}://${location.host}/ws/fan_group?services=${encodeURIComponent(servicesCsv)}&tail=${tail}&token=${token}${pj}`;
}
@@ -3613,7 +3690,17 @@ if (els.groupBtn && els.groupBtn.onclick !== null) {
// Функция для обновления счетчиков через Ajax
async function updateCounters(containerId) {
try {
const response = await fetch(`/api/logs/stats/${containerId}`);
const token = localStorage.getItem('access_token');
if (!token) {
console.error('No access token found');
return;
}
const response = await fetch(`/api/logs/stats/${containerId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const stats = await response.json();
const cdbg = document.querySelector('.cdbg');
@@ -4176,12 +4263,24 @@ document.addEventListener('DOMContentLoaded', () => {
// Обработчик для кнопки выхода
if (els.logoutBtn) {
els.logoutBtn.addEventListener('click', () => {
els.logoutBtn.addEventListener('click', async () => {
if (confirm('Вы уверены, что хотите выйти?')) {
// Очищаем localStorage
localStorage.clear();
// Перенаправляем на страницу входа
window.location.href = '/';
try {
// Вызываем API для выхода
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
} catch (error) {
console.error('Logout error:', error);
} finally {
// Очищаем localStorage
localStorage.removeItem('access_token');
// Перенаправляем на страницу входа
window.location.href = '/login';
}
}
});
}
@@ -4398,8 +4497,37 @@ window.addEventListener('keydown', async (e)=>{
// Инициализация
(async function init() {
console.log('Initializing LogBoard+...');
// Проверяем авторизацию
const token = localStorage.getItem('access_token');
if (!token) {
console.log('No access token found, redirecting to login');
window.location.href = '/login';
return;
}
// Проверяем валидность токена
try {
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
console.log('Invalid token, redirecting to login');
localStorage.removeItem('access_token');
window.location.href = '/login';
return;
}
} catch (error) {
console.error('Error checking auth:', error);
localStorage.removeItem('access_token');
window.location.href = '/login';
return;
}
console.log('Elements found:', {
containerList: !!els.containerList,
logTitle: !!els.logTitle,
logContent: !!els.logContent,