diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md deleted file mode 100644 index 3829715..0000000 --- a/MIGRATION_GUIDE.md +++ /dev/null @@ -1,151 +0,0 @@ -# Руководство по миграции с Basic Auth на JWT - -## 🔄 Миграция с Basic Auth на JWT авторизацию - -Это руководство поможет вам перейти с устаревшей системы Basic Auth на современную систему авторизации на основе JWT токенов. - -## 📋 Что изменилось - -### ✅ Новые возможности: -- **JWT токены** вместо Basic Auth -- **Страница входа** с современным интерфейсом -- **Безопасные сессии** с автоматическим истечением -- **Защищенные API** эндпоинты -- **Автоматическое перенаправление** на страницу входа - -### 🔧 Технические изменения: -- Обновлен `app.py` с новой системой авторизации -- Добавлена страница входа `templates/login.html` -- Обновлен фронтенд для работы с JWT токенами -- Добавлены новые зависимости в `requirements.txt` -- Обновлены переменные окружения - -## 🚀 Быстрая миграция - -### 1. Обновление зависимостей -```bash -# Остановите текущий сервис -make down - -# Обновите зависимости -pip install -r requirements.txt -# или для Docker -docker compose build --no-cache -``` - -### 2. Обновление переменных окружения -```bash -# Обновите .env файл -LOGBOARD_USER=admin # Ваше имя пользователя -LOGBOARD_PASS=admin123 # Ваш пароль -SECRET_KEY=your-secret-key # Уникальный секретный ключ -AUTH_METHOD=jwt # Изменено с basic на jwt -SESSION_TIMEOUT=3600 # Время жизни сессии в секундах -``` - -### 3. Запуск обновленного сервиса -```bash -# Запустите обновленный сервис -make up - -# Проверьте работу -make test-auth -``` - -## 🔐 Настройка безопасности - -### Рекомендуемые настройки для продакшена: - -```bash -# Генерируйте уникальные ключи -SECRET_KEY=$(openssl rand -hex 32) -ENCRYPTION_KEY=$(openssl rand -hex 32) - -# Используйте сложные пароли -LOGBOARD_PASS=YourComplexPassword123! - -# Настройте время жизни сессии -SESSION_TIMEOUT=3600 # 1 час -``` - -### Переменные окружения: - -| Переменная | Описание | Значение по умолчанию | -|------------|----------|----------------------| -| `LOGBOARD_USER` | Имя пользователя | `admin` | -| `LOGBOARD_PASS` | Пароль | `admin123` | -| `SECRET_KEY` | Секретный ключ для JWT | `your-secret-key-here` | -| `AUTH_METHOD` | Метод авторизации | `jwt` | -| `SESSION_TIMEOUT` | Время жизни сессии (сек) | `3600` | - -## 🧪 Тестирование - -### Автоматическое тестирование: -```bash -# Запустите тесты авторизации -make test-auth -``` - -### Ручное тестирование: -1. Откройте браузер: `http://localhost:9001` -2. Должны быть перенаправлены на страницу входа -3. Введите логин и пароль -4. Проверьте доступ к панели управления - -## 🔄 API изменения - -### Новые эндпоинты: -- `POST /api/auth/login` - вход в систему -- `POST /api/auth/logout` - выход из системы -- `GET /api/auth/me` - информация о текущем пользователе - -### Изменения в существующих эндпоинтах: -Все API эндпоинты теперь требуют JWT токен в заголовке: -``` -Authorization: Bearer -``` - -### WebSocket изменения: -WebSocket соединения теперь используют JWT токены вместо base64 токенов. - -## 🐛 Устранение неполадок - -### Проблема: "Unauthorized" ошибки -**Решение:** Проверьте правильность логина и пароля в `.env` файле - -### Проблема: Токен истекает слишком быстро -**Решение:** Увеличьте `SESSION_TIMEOUT` в настройках - -### Проблема: Не работает WebSocket -**Решение:** Убедитесь, что JWT токен передается в URL параметре `token` - -### Проблема: Страница входа не загружается -**Решение:** Проверьте, что `templates/login.html` существует и доступен - -## 📝 Логи изменений - -### Версия 2.0.0: -- ✅ Удален Basic Auth -- ✅ Добавлена JWT авторизация -- ✅ Создана страница входа -- ✅ Обновлен фронтенд -- ✅ Добавлены тесты авторизации -- ✅ Обновлена документация - -## 🆘 Поддержка - -Если у вас возникли проблемы с миграцией: - -1. Проверьте логи: `make logs` -2. Запустите тесты: `make test-auth` -3. Проверьте настройки в `.env` файле -4. Убедитесь, что все зависимости установлены - -## 📞 Контакты - -**Автор:** Сергей Антропов -**Сайт:** https://devops.org.ru - ---- - -**Примечание:** После миграции старые Basic Auth токены больше не будут работать. Все пользователи должны будут войти заново через новую систему авторизации. diff --git a/app.py b/app.py index 0036146..318aedc 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,8 @@ from pathlib import Path import docker from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query, Depends, HTTPException, status, Body, Request, Response +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.staticfiles import StaticFiles @@ -52,6 +54,182 @@ app = FastAPI( version="1.0.0" ) + + +# Обработчики исключений +@app.exception_handler(404) +async def not_found_handler(request: Request, exc: HTTPException): + """Обработчик ошибки 404 - Страница не найдена""" + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": 404, + "error_title": "Страница не найдена", + "error_message": "Запрашиваемая страница не существует или была перемещена." + }, status_code=404) + +@app.exception_handler(401) +async def unauthorized_handler(request: Request, exc: HTTPException): + """Обработчик ошибки 401 - Не авторизован""" + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": 401, + "error_title": "Требуется авторизация", + "error_message": "Для доступа к этой странице необходимо войти в систему." + }, status_code=401) + +@app.exception_handler(403) +async def forbidden_handler(request: Request, exc: HTTPException): + """Обработчик ошибки 403 - Доступ запрещен""" + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": 403, + "error_title": "Доступ запрещен", + "error_message": "У вас нет прав для доступа к этой странице." + }, status_code=403) + +@app.exception_handler(500) +async def internal_server_error_handler(request: Request, exc: HTTPException): + """Обработчик ошибки 500 - Внутренняя ошибка сервера""" + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": 500, + "error_title": "Внутренняя ошибка сервера", + "error_message": "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору." + }, status_code=500) + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """Общий обработчик HTTP исключений""" + # Для API маршрутов возвращаем JSON ответ + if request.url.path.startswith("/api/"): + if exc.status_code == 401: + return JSONResponse( + status_code=401, + content={ + "error": "unauthorized", + "message": "Требуется авторизация", + "details": "Для доступа к этому API необходимо войти в систему." + }, + headers={"WWW-Authenticate": "Bearer"} + ) + elif exc.status_code == 403: + return JSONResponse( + status_code=403, + content={ + "error": "forbidden", + "message": "Доступ запрещен", + "details": "У вас нет прав для доступа к этому API." + } + ) + else: + return JSONResponse( + status_code=exc.status_code, + content={ + "error": f"http_{exc.status_code}", + "message": exc.detail or "Произошла ошибка при обработке запроса.", + "details": f"URL: {request.url.path}" + } + ) + + # Для обычных страниц возвращаем HTML + if exc.status_code == 401: + title = "Требуется авторизация" + message = "Для доступа к этой странице необходимо войти в систему." + elif exc.status_code == 403: + title = "Доступ запрещен" + message = "У вас нет прав для доступа к этой странице." + elif exc.status_code == 404: + title = "Страница не найдена" + message = "Запрашиваемая страница не существует или была перемещена." + elif exc.status_code == 500: + title = "Внутренняя ошибка сервера" + message = "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору." + else: + title = f"Ошибка {exc.status_code}" + message = exc.detail or "Произошла ошибка при обработке запроса." + + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": exc.status_code, + "error_title": title, + "error_message": message + }, status_code=exc.status_code) + +@app.exception_handler(StarletteHTTPException) +async def starlette_http_exception_handler(request: Request, exc: StarletteHTTPException): + """Обработчик Starlette HTTP исключений (включая ошибки безопасности)""" + # Для API маршрутов возвращаем JSON ответ + if request.url.path.startswith("/api/"): + if exc.status_code == 401: + return JSONResponse( + status_code=401, + content={ + "error": "unauthorized", + "message": "Требуется авторизация", + "details": "Для доступа к этому API необходимо войти в систему." + }, + headers={"WWW-Authenticate": "Bearer"} + ) + elif exc.status_code == 403: + return JSONResponse( + status_code=403, + content={ + "error": "forbidden", + "message": "Доступ запрещен", + "details": "У вас нет прав для доступа к этому API." + } + ) + else: + return JSONResponse( + status_code=exc.status_code, + content={ + "error": f"http_{exc.status_code}", + "message": exc.detail or "Произошла ошибка при обработке запроса.", + "details": f"URL: {request.url.path}" + } + ) + + # Для обычных страниц возвращаем HTML + if exc.status_code == 401: + title = "Требуется авторизация" + message = "Для доступа к этой странице необходимо войти в систему." + elif exc.status_code == 403: + title = "Доступ запрещен" + message = "У вас нет прав для доступа к этой странице." + elif exc.status_code == 404: + title = "Страница не найдена" + message = "Запрашиваемая страница не существует или была перемещена." + elif exc.status_code == 500: + title = "Внутренняя ошибка сервера" + message = "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору." + else: + title = f"Ошибка {exc.status_code}" + message = exc.detail or "Произошла ошибка при обработке запроса." + + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": exc.status_code, + "error_title": title, + "error_message": message + }, status_code=exc.status_code) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """Общий обработчик всех исключений""" + import traceback + + # Логируем ошибку + print(f"❌ Необработанная ошибка: {exc}") + print(f"❌ URL: {request.url.path}") + print(f"❌ Traceback: {traceback.format_exc()}") + + return templates.TemplateResponse("error.html", { + "request": request, + "error_code": 500, + "error_title": "Внутренняя ошибка сервера", + "error_message": "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору." + }, status_code=500) + # Инициализация шаблонов templates = Jinja2Templates(directory="templates") @@ -119,7 +297,7 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s if username is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Недействительный токен", + detail="Недействительный токен. Требуется авторизация.", headers={"WWW-Authenticate": "Bearer"}, ) return username @@ -410,7 +588,7 @@ async def login(user_data: UserLogin, response: Response): else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Неверное имя пользователя или пароль", + detail="Неверное имя пользователя или пароль. Проверьте учетные данные и попробуйте снова.", headers={"WWW-Authenticate": "Bearer"}, ) @@ -430,6 +608,32 @@ def healthz(): """Health check endpoint""" return "ok" +# Маршруты для тестирования страниц ошибок (только в режиме разработки) +@app.get("/test/error/404") +async def test_404_error(): + """Тест страницы ошибки 404""" + raise HTTPException(status_code=404, detail="Тестовая ошибка 404") + +@app.get("/test/error/401") +async def test_401_error(): + """Тест страницы ошибки 401""" + raise HTTPException(status_code=401, detail="Тестовая ошибка 401") + +@app.get("/test/error/403") +async def test_403_error(): + """Тест страницы ошибки 403""" + raise HTTPException(status_code=403, detail="Тестовая ошибка 403") + +@app.get("/test/error/500") +async def test_500_error(): + """Тест страницы ошибки 500""" + raise HTTPException(status_code=500, detail="Тестовая ошибка 500") + +@app.get("/test/error/general") +async def test_general_error(): + """Тест общей ошибки""" + raise Exception("Тестовая общая ошибка") + @app.get("/api/logs/stats/{container_id}") def api_logs_stats(container_id: str, current_user: str = Depends(get_current_user)): """Получить статистику логов контейнера""" @@ -506,7 +710,10 @@ def api_update_excluded_containers( } ) else: - raise HTTPException(status_code=500, detail="Ошибка сохранения списка") + raise HTTPException( + status_code=500, + detail="Ошибка сохранения списка исключенных контейнеров. Попробуйте еще раз или обратитесь к администратору." + ) @app.get("/api/projects") def api_projects(current_user: str = Depends(get_current_user)): diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..b7fe2a7 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,259 @@ + + + + + + {{ error_title }} - LogBoard+ + + + +
+
+ {% if error_code == 401 %} + 🔐 + {% elif error_code == 403 %} + 🚫 + {% elif error_code == 404 %} + 🔍 + {% elif error_code == 500 %} + ⚠️ + {% else %} + ❌ + {% endif %} +
+ +
{{ error_code }}
+
{{ error_title }}
+ + {% if error_code == 401 %} +
+ 🔐 Эта страница требует авторизации +
+ {% elif error_code == 403 %} +
+ 🚫 У вас нет прав для доступа к этой странице +
+ {% endif %} + + {% if error_code != 403 %} +
+ {{ error_message }} +
+ {% endif %} + + + +
+ На главную + {% if error_code == 403 %} + Войти в систему + {% endif %} + +
+ + +
+ + + + diff --git a/templates/index.html b/templates/index.html index ba8a4b4..918d2ee 100644 --- a/templates/index.html +++ b/templates/index.html @@ -562,6 +562,15 @@ a{color:var(--link)} background: #c82333; } +.other-btn { + background: #6c757d; + color: white; +} + +.other-btn:hover { + background: #5a6268; +} + .btn-group { display: flex; gap: 8px; @@ -1313,6 +1322,10 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} ERROR 0 + @@ -1341,7 +1354,7 @@ const state = { open: {}, // id -> {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName} layout: 'tabs', // 'tabs' | 'grid2' | 'grid3' | 'grid4' filter: null, - levels: {debug:true, info:true, warn:true, err:true}, + levels: {debug:true, info:true, warn:true, err:true, other:true}, selectedContainers: [], // Массив ID выбранных контейнеров для мультипросмотра multiViewMode: false, // Режим мультипросмотра }; @@ -1365,6 +1378,7 @@ const els = { lvlInfo: document.getElementById('lvlInfo'), lvlWarn: document.getElementById('lvlWarn'), lvlErr: document.getElementById('lvlErr'), + lvlOther: document.getElementById('lvlOther'), layoutBadge: document.getElementById('layoutBadge') || { textContent: '' }, aggregate: document.getElementById('aggregate') || { checked: false }, themeSwitch: document.getElementById('themeSwitch'), @@ -1528,7 +1542,7 @@ function allowedByLevel(cls){ if (cls==='err') return state.levels.err; if (cls==='warn') return state.levels.warn; if (cls==='ok') return state.levels.info; - if (cls==='other') return true; // Всегда показываем неклассифицированные строки + if (cls==='other') return state.levels.other; // Показываем неклассифицированные строки в зависимости от настройки return true; } function applyFilter(line){ @@ -2345,7 +2359,7 @@ function openMultiViewWs(service) { serviceName: service.service, logEl: logEl, wrapEl: logEl, - counters: {dbg:0, info:0, warn:0, err:0}, + counters: {dbg:0, info:0, warn:0, err:0, other:0}, pausedBuffer: [], allLogs: [] // Добавляем буфер для логов }; @@ -2543,7 +2557,8 @@ function openWs(svc, panel){ const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo'); const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn'); const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr'); - const counters = {dbg:0,info:0,warn:0,err:0}; + const cother = panel.querySelector('.cother') || document.querySelector('.cother'); + const counters = {dbg:0,info:0,warn:0,err:0,other:0}; const ws = new WebSocket(wsUrl(id, (svc.service||svc.name), svc.project||'')); state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: (svc.service||svc.name)}; @@ -2621,6 +2636,7 @@ function openWs(svc, panel){ cinfo.textContent = counters.info; cwarn.textContent = counters.warn; cerr.textContent = counters.err; + if (cother) cother.textContent = counters.other; }; // Убираем автоматический refresh - теперь только по кнопке @@ -3253,7 +3269,7 @@ function handleLine(id, line){ // Отладочная информация для первых нескольких строк if (!obj.counters) { console.error(`handleLine: Counters not initialized for container ${id}`); - obj.counters = {dbg:0, info:0, warn:0, err:0}; + obj.counters = {dbg:0, info:0, warn:0, err:0, other:0}; } // Фильтруем сообщение "Connected to container" для всех режимов @@ -3278,6 +3294,7 @@ function handleLine(id, line){ if (cls==='ok') obj.counters.info++; if (cls==='warn') obj.counters.warn++; if (cls==='err') obj.counters.err++; + if (cls==='other') obj.counters.other++; } // Для Single View НЕ добавляем перенос строки после каждой строки лога @@ -3643,7 +3660,8 @@ function openFanGroup(services){ const cinfo = panel.querySelector('.cinfo') || document.querySelector('.cinfo'); const cwarn = panel.querySelector('.cwarn') || document.querySelector('.cwarn'); const cerr = panel.querySelector('.cerr') || document.querySelector('.cerr'); - const counters = {dbg:0,info:0,warn:0,err:0}; + const cother = panel.querySelector('.cother') || document.querySelector('.cother'); + const counters = {dbg:0,info:0,warn:0,err:0,other:0}; const ws = new WebSocket(fanGroupUrl(services.join(','), fake.project||'')); state.open[id] = {ws, logEl, wrapEl, counters, pausedBuffer:[], serviceName: ('group:'+services.join(','))}; @@ -3663,6 +3681,7 @@ function openFanGroup(services){ cinfo.textContent = counters.info; cwarn.textContent = counters.warn; cerr.textContent = counters.err; + if (cother) cother.textContent = counters.other; // Обновляем видимость счетчиков updateCounterVisibility(); @@ -3707,11 +3726,13 @@ async function updateCounters(containerId) { const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); + const cother = document.querySelector('.cother'); if (cdbg) cdbg.textContent = stats.debug || 0; if (cinfo) cinfo.textContent = stats.info || 0; if (cwarn) cwarn.textContent = stats.warn || 0; if (cerr) cerr.textContent = stats.error || 0; + if (cother) cother.textContent = stats.other || 0; // Обновляем видимость счетчиков updateCounterVisibility(); @@ -3779,7 +3800,7 @@ function recalculateCounters() { const visibleLogs = obj.allLogs.slice(-tailLines); // Сбрасываем счетчики - obj.counters = {dbg: 0, info: 0, warn: 0, err: 0}; + obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0}; // Пересчитываем счетчики только для отображаемых логов visibleLogs.forEach(logEntry => { @@ -3789,6 +3810,7 @@ function recalculateCounters() { if (logEntry.cls === 'ok') obj.counters.info++; if (logEntry.cls === 'warn') obj.counters.warn++; if (logEntry.cls === 'err') obj.counters.err++; + if (logEntry.cls === 'other') obj.counters.other++; } }); @@ -3797,11 +3819,13 @@ function recalculateCounters() { const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); + const cother = document.querySelector('.cother'); if (cdbg) cdbg.textContent = obj.counters.dbg; if (cinfo) cinfo.textContent = obj.counters.info; if (cwarn) cwarn.textContent = obj.counters.warn; if (cerr) cerr.textContent = obj.counters.err; + if (cother) cother.textContent = obj.counters.other; console.log(`Counters recalculated for container ${containerId} (tail: ${tailLines}):`, obj.counters); } @@ -3822,6 +3846,7 @@ function recalculateMultiViewCounters() { let totalInfo = 0; let totalWarn = 0; let totalError = 0; + let totalOther = 0; // Пересчитываем счетчики для каждого контейнера for (const containerId of state.selectedContainers) { @@ -3832,7 +3857,7 @@ function recalculateMultiViewCounters() { const visibleLogs = obj.allLogs.slice(-tailLines); // Сбрасываем счетчики для этого контейнера - obj.counters = {dbg: 0, info: 0, warn: 0, err: 0}; + obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0}; // Пересчитываем счетчики только для отображаемых логов visibleLogs.forEach(logEntry => { @@ -3842,6 +3867,7 @@ function recalculateMultiViewCounters() { if (logEntry.cls === 'ok') obj.counters.info++; if (logEntry.cls === 'warn') obj.counters.warn++; if (logEntry.cls === 'err') obj.counters.err++; + if (logEntry.cls === 'other') obj.counters.other++; } }); @@ -3850,6 +3876,7 @@ function recalculateMultiViewCounters() { totalInfo += obj.counters.info; totalWarn += obj.counters.warn; totalError += obj.counters.err; + totalOther += obj.counters.other; } // Обновляем отображение счетчиков @@ -3857,13 +3884,15 @@ function recalculateMultiViewCounters() { const cinfo = document.querySelector('.cinfo'); const cwarn = document.querySelector('.cwarn'); const cerr = document.querySelector('.cerr'); + const cother = document.querySelector('.cother'); if (cdbg) cdbg.textContent = totalDebug; if (cinfo) cinfo.textContent = totalInfo; if (cwarn) cwarn.textContent = totalWarn; if (cerr) cerr.textContent = totalError; + if (cother) cother.textContent = totalOther; - console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError }); + console.log(`Multi-view counters recalculated (tail: ${tailLines}):`, { totalDebug, totalInfo, totalWarn, totalError, totalOther }); } // Функция для обновления видимости счетчиков @@ -3872,6 +3901,7 @@ function updateCounterVisibility() { const infoBtn = document.querySelector('.info-btn'); const warnBtn = document.querySelector('.warn-btn'); const errorBtn = document.querySelector('.error-btn'); + const otherBtn = document.querySelector('.other-btn'); if (debugBtn) { debugBtn.classList.toggle('disabled', !state.levels.debug); @@ -3885,6 +3915,9 @@ function updateCounterVisibility() { if (errorBtn) { errorBtn.classList.toggle('disabled', !state.levels.err); } + if (otherBtn) { + otherBtn.classList.toggle('disabled', !state.levels.other); + } } // Функция для обновления логов и счетчиков @@ -4020,6 +4053,7 @@ function addCounterClickHandlers() { const infoBtn = document.querySelector('.info-btn'); const warnBtn = document.querySelector('.warn-btn'); const errorBtn = document.querySelector('.error-btn'); + const otherBtn = document.querySelector('.other-btn'); if (debugBtn) { debugBtn.onclick = () => { @@ -4084,6 +4118,22 @@ function addCounterClickHandlers() { } }; } + + if (otherBtn) { + otherBtn.onclick = () => { + state.levels.other = !state.levels.other; + updateCounterVisibility(); + refreshAllLogs(); + // Добавляем refresh для обновления логов + if (state.current) { + refreshLogsAndCounters(); + } + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + } + }; + } } @@ -4464,6 +4514,25 @@ if (els.lvlErr) { } }; } +if (els.lvlOther) { + els.lvlOther.onchange = ()=> { + state.levels.other = els.lvlOther.checked; + updateCounterVisibility(); + refreshAllLogs(); + // Обновляем multi-view если он активен + if (state.multiViewMode) { + refreshAllLogs(); + setTimeout(() => { + recalculateMultiViewCounters(); + }, 100); + } else { + // Пересчитываем счетчики для Single View + setTimeout(() => { + recalculateCounters(); + }, 100); + } + }; +} // Hotkeys: [ ] — navigation between containers window.addEventListener('keydown', async (e)=>{ diff --git a/test_auth.py b/test_auth.py deleted file mode 100644 index 72c75d8..0000000 --- a/test_auth.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Тестовый скрипт для проверки новой системы авторизации LogBoard+ -Автор: Сергей Антропов -Сайт: https://devops.org.ru -""" - -import requests -import json -import sys -from datetime import datetime - -# Настройки -BASE_URL = "http://localhost:9001" -USERNAME = "admin" -PASSWORD = "admin123" - -def test_login(): - """Тест входа в систему""" - print("🔐 Тестирование входа в систему...") - - try: - response = requests.post( - f"{BASE_URL}/api/auth/login", - json={ - "username": USERNAME, - "password": PASSWORD - }, - headers={"Content-Type": "application/json"} - ) - - if response.status_code == 200: - data = response.json() - print(f"✅ Успешный вход! Получен токен: {data['access_token'][:20]}...") - return data['access_token'] - else: - print(f"❌ Ошибка входа: {response.status_code} - {response.text}") - return None - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return None - -def test_protected_endpoint(token): - """Тест защищенного эндпоинта""" - print("\n🔒 Тестирование защищенного эндпоинта...") - - try: - response = requests.get( - f"{BASE_URL}/api/auth/me", - headers={"Authorization": f"Bearer {token}"} - ) - - if response.status_code == 200: - data = response.json() - print(f"✅ Доступ к защищенному эндпоинту: {data}") - return True - else: - print(f"❌ Ошибка доступа: {response.status_code} - {response.text}") - return False - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return False - -def test_projects_api(token): - """Тест API проектов""" - print("\n📋 Тестирование API проектов...") - - try: - response = requests.get( - f"{BASE_URL}/api/projects", - headers={"Authorization": f"Bearer {token}"} - ) - - if response.status_code == 200: - projects = response.json() - print(f"✅ Получен список проектов: {projects}") - return True - else: - print(f"❌ Ошибка получения проектов: {response.status_code} - {response.text}") - return False - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return False - -def test_services_api(token): - """Тест API сервисов""" - print("\n🐳 Тестирование API сервисов...") - - try: - response = requests.get( - f"{BASE_URL}/api/services", - headers={"Authorization": f"Bearer {token}"} - ) - - if response.status_code == 200: - services = response.json() - print(f"✅ Получен список сервисов: {len(services)} контейнеров") - return True - else: - print(f"❌ Ошибка получения сервисов: {response.status_code} - {response.text}") - return False - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return False - -def test_logout(token): - """Тест выхода из системы""" - print("\n🚪 Тестирование выхода из системы...") - - try: - response = requests.post( - f"{BASE_URL}/api/auth/logout", - headers={"Authorization": f"Bearer {token}"} - ) - - if response.status_code == 200: - print("✅ Успешный выход из системы") - return True - else: - print(f"❌ Ошибка выхода: {response.status_code} - {response.text}") - return False - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return False - -def test_unauthorized_access(): - """Тест доступа без авторизации""" - print("\n🚫 Тестирование доступа без авторизации...") - - try: - response = requests.get(f"{BASE_URL}/api/projects") - - if response.status_code == 401: - print("✅ Правильно отклонен доступ без авторизации") - return True - else: - print(f"❌ Неожиданный ответ: {response.status_code} - {response.text}") - return False - - except Exception as e: - print(f"❌ Ошибка соединения: {e}") - return False - -def main(): - """Основная функция тестирования""" - print("🧪 Тестирование новой системы авторизации LogBoard+") - print("=" * 60) - print(f"📅 Время тестирования: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print(f"🌐 URL: {BASE_URL}") - print(f"👤 Пользователь: {USERNAME}") - print("=" * 60) - - # Проверяем доступность сервера - try: - response = requests.get(f"{BASE_URL}/healthz") - if response.status_code != 200: - print("❌ Сервер недоступен") - sys.exit(1) - print("✅ Сервер доступен") - except Exception as e: - print(f"❌ Сервер недоступен: {e}") - sys.exit(1) - - # Тестируем вход - token = test_login() - if not token: - print("❌ Тест провален: не удалось войти в систему") - sys.exit(1) - - # Тестируем защищенные эндпоинты - success = True - success &= test_protected_endpoint(token) - success &= test_projects_api(token) - success &= test_services_api(token) - - # Тестируем выход - success &= test_logout(token) - - # Тестируем доступ без авторизации - success &= test_unauthorized_access() - - print("\n" + "=" * 60) - if success: - print("🎉 Все тесты пройдены успешно!") - print("✅ Новая система авторизации работает корректно") - else: - print("❌ Некоторые тесты провалились") - print("🔧 Проверьте настройки и логи сервера") - - print("=" * 60) - -if __name__ == "__main__": - main()