""" 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": "Пароль успешно изменен"}