from datetime import datetime, timezone from typing import Dict, Any, Optional from fastapi import Depends, HTTPException, status, Request from fastapi.security import OAuth2PasswordBearer from .db import get_user_by_username, upsert_user_activity from .security import safe_decode_token, TokenError oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") def _is_expired(expires_at: str | None) -> bool: if not isinstance(expires_at, str) or not expires_at.strip(): return False candidate = expires_at.strip() if candidate.endswith("Z"): candidate = candidate[:-1] + "+00:00" try: parsed = datetime.fromisoformat(candidate) except ValueError: return False if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed <= datetime.now(timezone.utc) def _extract_client_ip(request: Request) -> str: forwarded = request.headers.get("x-forwarded-for") if forwarded: parts = [part.strip() for part in forwarded.split(",") if part.strip()] if parts: return parts[0] real_ip = request.headers.get("x-real-ip") if real_ip: return real_ip.strip() if request.client and request.client.host: return request.client.host return "unknown" def _load_current_user_from_token(token: str, request: Optional[Request] = None) -> Dict[str, Any]: try: payload = safe_decode_token(token) except TokenError as exc: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc username = payload.get("sub") if not username: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token subject") user = get_user_by_username(username) if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") if user.get("is_blocked"): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is blocked") if _is_expired(user.get("expires_at")): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User access has expired") if request is not None: ip = _extract_client_ip(request) user_agent = request.headers.get("user-agent", "unknown") upsert_user_activity(user["username"], ip, user_agent) return { "username": user["username"], "role": user["role"], "auth_provider": user.get("auth_provider", "local"), "jellyseerr_user_id": user.get("jellyseerr_user_id"), "auto_search_enabled": bool(user.get("auto_search_enabled", True)), "profile_id": user.get("profile_id"), "expires_at": user.get("expires_at"), "is_expired": bool(user.get("is_expired", False)), } def get_current_user(token: str = Depends(oauth2_scheme), request: Request = None) -> Dict[str, Any]: return _load_current_user_from_token(token, request) def get_current_user_event_stream(request: Request) -> Dict[str, Any]: """EventSource cannot send Authorization headers, so allow a query token here only.""" token = None auth_header = request.headers.get("authorization", "") if auth_header.lower().startswith("bearer "): token = auth_header.split(" ", 1)[1].strip() if not token: token = request.query_params.get("access_token") if not token: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing token") return _load_current_user_from_token(token, None) def require_admin(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: if user.get("role") != "admin": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return user def require_admin_event_stream( user: Dict[str, Any] = Depends(get_current_user_event_stream), ) -> Dict[str, Any]: if user.get("role") != "admin": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return user