feat: добавлены страницы ошибок и кнопка OTHER в LogLevels
- Добавлена кнопка OTHER в LogLevels для неклассифицированных логов - Созданы красивые страницы ошибок с поддержкой темной/светлой темы - Добавлены обработчики для ошибок 401, 403, 404, 500 - Реализована безопасность: убраны детали ошибок из пользовательского интерфейса - Кнопка 'Войти в систему' показывается только на странице ошибки 403 - На странице 403 убран error-message, оставлен только auth-notice - Обновлены счетчики логов для поддержки уровня OTHER - Добавлены тестовые маршруты для проверки страниц ошибок - Улучшен UX: адаптивный дизайн, интерактивность, навигация Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
213
app.py
213
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)):
|
||||
|
||||
Reference in New Issue
Block a user