from fastapi import APIRouter, HTTPException, Request, Response, status from sqlalchemy import select from app.config import get_settings from app.deps import DB, SessionUser from app.models import User from app.ratelimit import client_ip, login_limiter from app.schemas import ChangeCredentialsIn, LoginIn, MeOut from app.security import ( COOKIE_NAME, create_token, hash_password, validate_new_password, validate_username, verify_password, ) router = APIRouter(prefix="/api/auth", tags=["auth"]) def _set_cookie(response: Response, user: User) -> None: settings = get_settings() response.set_cookie( COOKIE_NAME, create_token(user.id, user.token_version), max_age=settings.session_ttl_hours * 3600, httponly=True, samesite="lax", secure=settings.cookie_secure, ) @router.post("/login", response_model=MeOut) async def login(body: LoginIn, request: Request, response: Response, db: DB) -> User: keys = (f"ip:{client_ip(request)}", f"user:{body.username.lower()}") if wait := login_limiter.retry_after(*keys): raise HTTPException( status.HTTP_429_TOO_MANY_REQUESTS, f"Слишком много неудачных попыток входа. Повторите через {wait} с", headers={"Retry-After": str(wait)}, ) user = await db.scalar(select(User).where(User.username == body.username)) if not user or not user.is_active or not verify_password(user.password_hash, body.password): login_limiter.fail(*keys) raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Неверный логин или пароль") login_limiter.reset(*keys) _set_cookie(response, user) return user @router.post("/logout", status_code=204) async def logout(response: Response) -> None: response.delete_cookie(COOKIE_NAME) @router.get("/me", response_model=MeOut) async def me(user: SessionUser) -> User: return user @router.post("/change-credentials", response_model=MeOut) async def change_credentials( body: ChangeCredentialsIn, response: Response, user: SessionUser, db: DB ) -> User: if not verify_password(user.password_hash, body.current_password): raise HTTPException(status.HTTP_400_BAD_REQUEST, "Текущий пароль неверен") if body.new_password == body.current_password: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Новый пароль совпадает со старым") if err := validate_new_password(body.new_password): raise HTTPException(status.HTTP_400_BAD_REQUEST, err) new_username = (body.new_username or "").strip() if user.must_change_username and (not new_username or new_username == user.username): raise HTTPException(status.HTTP_400_BAD_REQUEST, "Необходимо задать новый логин") if new_username and new_username != user.username: if err := validate_username(new_username): raise HTTPException(status.HTTP_400_BAD_REQUEST, err) if await db.scalar(select(User.id).where(User.username == new_username)): raise HTTPException(status.HTTP_409_CONFLICT, "Логин уже занят") user.username = new_username user.password_hash = hash_password(body.new_password) user.must_change_password = False user.must_change_username = False user.token_version += 1 await db.commit() _set_cookie(response, user) return user