feat: Добавлена новая система авторизации с JWT токенами
- Удален Basic Auth, заменен на современную JWT авторизацию - Добавлена страница входа с красивым интерфейсом - Обновлен фронтенд для работы с JWT токенами - Добавлены новые зависимости: PyJWT, passlib[bcrypt], jinja2 - Создан тестовый скрипт для проверки авторизации - Добавлено руководство по миграции - Обновлена документация и README - Улучшен дизайн поля ввода пароля на странице входа Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user