refactor: extract all routes to app/api/v1/endpoints/ with proper structure

This commit is contained in:
Сергей Антропов
2025-08-20 16:48:06 +03:00
parent 1e6149107d
commit 40f614304b
11 changed files with 1193 additions and 1153 deletions

82
app/core/auth.py Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LogBoard+ - Аутентификация
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import os
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from passlib.context import CryptContext
import jwt
from app.core.config import (
SECRET_KEY,
ALGORITHM,
ACCESS_TOKEN_EXPIRE_MINUTES,
ADMIN_USERNAME,
ADMIN_PASSWORD
)
# Инициализация безопасности
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Функции для работы с паролями
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Проверяет пароль"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Хеширует пароль"""
return pwd_context.hash(password)
# Функции для работы с JWT токенами
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""Создает JWT токен"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(token: str) -> Optional[str]:
"""Проверяет JWT токен и возвращает имя пользователя"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
return username
except jwt.PyJWTError:
return None
# Функция для проверки аутентификации
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
"""Получает текущего пользователя из токена"""
token = credentials.credentials
username = verify_token(token)
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен. Требуется авторизация.",
headers={"WWW-Authenticate": "Bearer"},
)
return username
# Функция для проверки пользователя
def authenticate_user(username: str, password: str) -> bool:
"""Аутентифицирует пользователя"""
if username == ADMIN_USERNAME:
# Для простоты используем прямое сравнение паролей
# В продакшене рекомендуется использовать хешированные пароли
return password == ADMIN_PASSWORD
return False

42
app/core/config.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LogBoard+ - Конфигурация приложения
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import os
from fastapi.templating import Jinja2Templates
# Настройки приложения
APP_PORT = int(os.getenv("LOGBOARD_PORT", "9001"))
DEFAULT_TAIL = int(os.getenv("LOGBOARD_TAIL", "500"))
DEFAULT_PROJECT = os.getenv("COMPOSE_PROJECT_NAME")
DEFAULT_PROJECTS = os.getenv("LOGBOARD_PROJECTS")
SKIP_UNHEALTHY = os.getenv("LOGBOARD_SKIP_UNHEALTHY", "true").lower() == "true"
CONTAINER_LIST_TIMEOUT = int(os.getenv("LOGBOARD_CONTAINER_LIST_TIMEOUT", "10"))
CONTAINER_INFO_TIMEOUT = int(os.getenv("LOGBOARD_CONTAINER_INFO_TIMEOUT", "3"))
HEALTH_CHECK_TIMEOUT = int(os.getenv("LOGBOARD_HEALTH_CHECK_TIMEOUT", "2"))
# Настройки безопасности
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("SESSION_TIMEOUT", "3600")) // 60 # 1 час по умолчанию
# Настройки пользователей
ADMIN_USERNAME = os.getenv("LOGBOARD_USER", "admin")
ADMIN_PASSWORD = os.getenv("LOGBOARD_PASS", "admin")
# Настройки AJAX обновления
AJAX_UPDATE_INTERVAL = int(os.getenv("LOGBOARD_AJAX_UPDATE_INTERVAL", "2000"))
# Настройки режима отладки
DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true"
# Инициализация шаблонов
templates = Jinja2Templates(directory="app/templates")
# Директории
SNAP_DIR = os.getenv("LOGBOARD_SNAPSHOT_DIR", "./snapshots")
STATIC_DIR = os.getenv("LOGBOARD_STATIC_DIR", "./app/static")

243
app/core/docker.py Normal file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LogBoard+ - Docker функции
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import json
import os
from typing import List, Dict, Optional
import docker
from app.core.config import (
DEFAULT_TAIL,
DEFAULT_PROJECT,
DEFAULT_PROJECTS,
SKIP_UNHEALTHY
)
# Инициализация Docker клиента
docker_client = docker.from_env()
def load_excluded_containers() -> List[str]:
"""
Загружает список исключенных контейнеров из JSON файла
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
try:
with open("app/excluded_containers.json", "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("excluded_containers", [])
except FileNotFoundError:
print("⚠️ Файл app/excluded_containers.json не найден, используем пустой список")
return []
except json.JSONDecodeError as e:
print(f"❌ Ошибка парсинга app/excluded_containers.json: {e}")
return []
except Exception as e:
print(f"❌ Ошибка загрузки app/excluded_containers.json: {e}")
return []
def save_excluded_containers(containers: List[str]) -> bool:
"""
Сохраняет список исключенных контейнеров в JSON файл
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
try:
data = {
"excluded_containers": containers,
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
}
with open("app/excluded_containers.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"❌ Ошибка сохранения app/excluded_containers.json: {e}")
return False
def get_all_projects() -> List[str]:
"""
Получает список всех проектов Docker Compose с учетом исключенных контейнеров
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
projects = set()
excluded_containers = load_excluded_containers()
try:
containers = docker_client.containers.list(all=True)
# Словарь для подсчета контейнеров по проектам
project_containers = {}
standalone_containers = []
for c in containers:
try:
# Пропускаем исключенные контейнеры
if c.name in excluded_containers:
continue
labels = c.labels or {}
project = labels.get("com.docker.compose.project")
if project:
if project not in project_containers:
project_containers[project] = 0
project_containers[project] += 1
else:
standalone_containers.append(c.name)
except Exception:
continue
# Добавляем проекты, у которых есть хотя бы один неисключенный контейнер
for project, count in project_containers.items():
if count > 0:
projects.add(project)
# Добавляем standalone, если есть неисключенные контейнеры без проекта
if standalone_containers:
projects.add("standalone")
except Exception as e:
print(f"❌ Ошибка получения списка проектов: {e}")
return []
result = sorted(list(projects))
print(f"📋 Доступные проекты (с учетом исключенных контейнеров): {result}")
return result
def list_containers(projects: Optional[List[str]] = None, include_stopped: bool = False) -> List[Dict]:
"""
Получает список контейнеров с поддержкой множественных проектов
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
# Загружаем список исключенных контейнеров из JSON файла
excluded_containers = load_excluded_containers()
print(f"🚫 Список исключенных контейнеров: {excluded_containers}")
items = []
excluded_count = 0
try:
# Получаем список контейнеров с базовой обработкой ошибок
containers = docker_client.containers.list(all=include_stopped)
for c in containers:
try:
# Базовая информация о контейнере (без health check)
basic_info = {
"id": c.id[:12],
"name": c.name,
"status": c.status,
"image": "unknown",
"service": c.name,
"project": None,
"health": None,
"ports": [],
"url": None,
}
# Безопасно получаем метки
try:
labels = c.labels or {}
basic_info["project"] = labels.get("com.docker.compose.project")
basic_info["service"] = labels.get("com.docker.compose.service") or c.name
except Exception:
pass # Используем значения по умолчанию
# Безопасно получаем информацию об образе
try:
if c.image and c.image.tags:
basic_info["image"] = c.image.tags[0]
elif c.image:
basic_info["image"] = c.image.short_id
except Exception:
pass # Оставляем "unknown"
# Безопасно получаем информацию о портах
try:
ports = c.ports or {}
if ports:
basic_info["ports"] = list(ports.keys())
# Пытаемся найти HTTP/HTTPS порт для создания URL
for port_mapping in ports.values():
if port_mapping:
for mapping in port_mapping:
if isinstance(mapping, dict) and mapping.get("HostPort"):
host_port = mapping["HostPort"]
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
host_port_int = int(host_port)
if (host_port_int in [80, 443] or
(1 <= host_port_int <= 7999) or
(8000 <= host_port_int <= 65535)):
protocol = "https" if host_port == "443" else "http"
basic_info["url"] = f"{protocol}://localhost:{host_port}"
basic_info["host_port"] = host_port
break
if basic_info["url"]:
break
except Exception:
pass # Оставляем пустые значения
# Фильтрация по проектам
if projects:
# Если проект не указан, считаем его standalone
container_project = basic_info["project"] or "standalone"
if container_project not in projects:
continue
# Фильтрация исключенных контейнеров
if basic_info["name"] in excluded_containers:
excluded_count += 1
print(f"⚠️ Пропускаем исключенный контейнер: {basic_info['name']}")
continue
# Добавляем контейнер в список
items.append(basic_info)
except Exception as e:
# Пропускаем контейнеры с критическими ошибками
print(f"⚠️ Пропускаем проблемный контейнер {c.name if hasattr(c, 'name') else 'unknown'} (ID: {c.id[:12]}): {e}")
continue
except Exception as e:
print(f"❌ Ошибка получения списка контейнеров: {e}")
return []
# Сортируем по проекту, сервису и имени
items.sort(key=lambda x: (x.get("project") or "", x.get("service") or "", x.get("name") or ""))
# Подсчитываем статистику по проектам
project_stats = {}
for item in items:
project = item.get("project") or "standalone"
if project not in project_stats:
project_stats[project] = {"visible": 0, "excluded": 0}
project_stats[project]["visible"] += 1
# Подсчитываем исключенные контейнеры по проектам
for c in containers:
try:
if c.name in excluded_containers:
labels = c.labels or {}
project = labels.get("com.docker.compose.project") or "standalone"
if project not in project_stats:
project_stats[project] = {"visible": 0, "excluded": 0}
project_stats[project]["excluded"] += 1
except Exception:
continue
print(f"📊 Статистика: найдено {len(items)} контейнеров, исключено {excluded_count} контейнеров")
for project, stats in project_stats.items():
print(f" 📦 {project}: {stats['visible']} видимых, {stats['excluded']} исключенных")
return items