feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View File

@@ -0,0 +1,184 @@
"""
API endpoints для аутентификации
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Depends, status, Form
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.security import OAuth2PasswordRequestForm
from pathlib import Path
from datetime import timedelta
from app.core.config import settings
from app.auth.security import create_access_token
from app.auth.deps import get_current_user
from app.db.session import get_async_db
from app.services.user_service import UserService
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""Страница входа"""
return templates.TemplateResponse(
"pages/auth/login.html",
{"request": request}
)
@router.post("/api/v1/auth/login")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для входа"""
# Убеждаемся, что пользователь admin существует
await UserService.ensure_admin_user(db)
# Получаем пользователя из БД
user = await UserService.get_user_by_username(db, form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем пароль
if not await UserService.verify_user_password(user, form_data.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Пользователь отключен"
)
# Если пароль был "admin" и не хеширован, обновляем его
if user.hashed_password == "admin" or len(user.hashed_password) < 50:
await UserService.update_password(db, user, form_data.password)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires
)
response = JSONResponse(content={
"access_token": access_token,
"token_type": "bearer"
})
# Устанавливаем токен в cookie
response.set_cookie(
key="access_token",
value=access_token,
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
httponly=True,
secure=False, # В продакшене должно быть True для HTTPS
samesite="lax"
)
return response
@router.get("/logout")
@router.head("/logout") # Поддержка HEAD запросов
async def logout():
"""Выход (удаляет токен из cookie и перенаправляет на страницу входа)
Доступен по путям:
- /logout (через прямой router в main.py)
- /api/v1/auth/logout (через api_router с префиксом /auth)
"""
from fastapi.responses import RedirectResponse
response = RedirectResponse(url="/login", status_code=302)
response.delete_cookie(
key="access_token",
httponly=True,
secure=False,
samesite="lax"
)
return response
@router.get("/api/v1/auth/me")
async def get_current_user_info(
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Получение информации о текущем пользователе"""
username = current_user.get("username")
if username:
user = await UserService.get_user_by_username(db, username)
if user:
return {
"username": user.username,
"is_active": user.is_active,
"is_superuser": user.is_superuser,
"created_at": user.created_at.isoformat() if user.created_at else None
}
return current_user
@router.get("/change-password", response_class=HTMLResponse)
async def change_password_page(request: Request, current_user: dict = Depends(get_current_user)):
"""Страница смены пароля"""
return templates.TemplateResponse(
"pages/auth/change-password.html",
{
"request": request,
"current_user": current_user
}
)
@router.post("/api/v1/auth/change-password")
async def change_password(
current_password: str = Form(...),
new_password: str = Form(...),
new_password_confirm: str = Form(...),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Смена пароля пользователя"""
# Проверка совпадения паролей
if new_password != new_password_confirm:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Новые пароли не совпадают"
)
username = current_user.get("username")
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не авторизован"
)
user = await UserService.get_user_by_username(db, username)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Проверяем текущий пароль
if not await UserService.verify_user_password(user, current_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный текущий пароль"
)
# Обновляем пароль
await UserService.update_password(db, user, new_password)
return {"message": "Пароль успешно изменен"}