from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel, field_validator from typing import Optional from datetime import datetime, timezone import uuid from bd import make_engine from bd.tables.users import User from route.auth_utils import hash_password, verify_password, create_access_token, SECRET_KEY, ALGORITHM, oauth2_scheme from jose import jwt, JWTError from redis_db import blacklist_token router = APIRouter(tags=["auth"], prefix="/auth") def get_session(): from sqlalchemy.orm import sessionmaker return sessionmaker(bind=make_engine(), future=True) class RegisterIn(BaseModel): username: str password: str first_name: str last_name: str group_id: Optional[str] = None organization_id: Optional[str] = None @field_validator("password") @classmethod def password_strength(cls, v: str) -> str: if len(v) < 8: raise ValueError("Password must be at least 8 characters") return v @field_validator("username") @classmethod def username_clean(cls, v: str) -> str: v = v.strip() if len(v) < 3: raise ValueError("Username must be at least 3 characters") return v @router.post("/register", status_code=201) def register(payload: RegisterIn): Session = get_session() with Session() as session: existing = session.query(User).filter(User.username == payload.username).first() if existing: raise HTTPException(status_code=400, detail="Username already taken") user = User( first_name=payload.first_name.strip(), last_name=payload.last_name.strip(), username=payload.username, hashed_password=hash_password(payload.password), group_id=uuid.UUID(payload.group_id) if payload.group_id else None, organization_id=uuid.UUID(payload.organization_id) if payload.organization_id else None, ) session.add(user) session.commit() session.refresh(user) return {"id": str(user.id), "username": user.username} @router.post("/login") def login(form: OAuth2PasswordRequestForm = Depends()): Session = get_session() with Session() as session: user = session.query(User).filter(User.username == form.username).first() if not user or not verify_password(form.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) token = create_access_token({"sub": str(user.id)}) return {"access_token": token, "token_type": "bearer"} @router.post("/logout") def logout(token: str = Depends(oauth2_scheme)): """Инвалидирует текущий Bearer-токен через Redis блэклист.""" try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) jti: str | None = payload.get("jti") exp: int | None = payload.get("exp") if jti and exp: ttl = max(0, exp - int(datetime.now(timezone.utc).timestamp())) blacklist_token(jti, ttl) except JWTError: pass # токен уже невалиден — ничего не делаем return {"detail": "Successfully logged out"}