feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
184
app/api/v1/endpoints/auth.py
Normal file
184
app/api/v1/endpoints/auth.py
Normal 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": "Пароль успешно изменен"}
|
||||
Reference in New Issue
Block a user