from datetime import datetime, timedelta from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session from backend.shared.utils import verify_token from backend.shared.models import User, DeviceSession from backend.shared.db import get_session_factory import logging security = HTTPBearer() logger = logging.getLogger("uvicorn.error") # Зависимость для получения сессии БД SessionLocal = get_session_factory() def get_db(): db = SessionLocal() try: yield db finally: db.close() # Зависимость для получения текущего пользователя def get_current_user( request: Request, credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db), ) -> User: token = credentials.credentials try: payload = verify_token(token) except Exception as e: logger.warning("get_current_user: token verification error: %s", str(e)) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}, ) if not payload: logger.info("get_current_user: verify_token returned empty payload") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}, ) user = db.query(User).filter(User.id == payload["user_id"]).first() if not user: logger.info("get_current_user: user not found for user_id=%s", payload.get("user_id")) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found", headers={"WWW-Authenticate": "Bearer"}, ) if user.id == 1 and user.suspended: user.suspended = False user.suspension_reason = None db.commit() db.refresh(user) # Validate device session from JWT session_id = payload.get("session_id") if not session_id: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session", headers={"WWW-Authenticate": "Bearer"}, ) device_session = ( db.query(DeviceSession) .filter(DeviceSession.user_id == user.id, DeviceSession.session_id == session_id) .first() ) if not device_session or device_session.revoked: logger.info("get_current_user: session missing/revoked for user_id=%s session_id=%s", user.id, session_id) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Session revoked or not found", headers={"WWW-Authenticate": "Bearer"}, ) # Check if session has been inactive for too long (sliding expiration) from backend.shared.constants import TOKEN_INACTIVITY_EXPIRE_HOURS inactivity_threshold = datetime.now() - timedelta(hours=TOKEN_INACTIVITY_EXPIRE_HOURS) if device_session.last_seen < inactivity_threshold: # Session expired due to inactivity - revoke it device_session.revoked = True db.commit() logger.info("get_current_user: session expired due to inactivity for user_id=%s session_id=%s", user.id, session_id) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired due to inactivity", headers={"WWW-Authenticate": "Bearer"}, ) # Touch last_seen on valid session (sliding expiration - extends token life) device_session.last_seen = datetime.now() db.commit() # Check if user is suspended if user.suspended: logger.info("get_current_user: account suspended for user_id=%s reason=%s", user.id, user.suspension_reason) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account suspended", headers={"suspension_reason": user.suspension_reason or "No reason provided"}, ) # Check if user is deleted if user.deleted: logger.info("get_current_user: account deleted for user_id=%s", user.id) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Account deleted", ) request.state.current_user = user request.state.session_id = session_id return user