from typing import Optional from fastapi import Depends, HTTPException, Request, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from motor.motor_asyncio import AsyncIOMotorDatabase from ..models.user import User, UserRole from .config import settings from .database import get_database from .security import decode_token security = HTTPBearer() # Roles that see all jobs (no tenant isolation) STAFF_ROLES = {UserRole.ADMIN, UserRole.REVIEWER, UserRole.LINGUIST, UserRole.PRODUCTION} async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: AsyncIOMotorDatabase = Depends(get_database), ) -> User: token = credentials.credentials payload = decode_token(token) user_id: str = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", ) user_doc = await db.users.find_one({"_id": user_id}) if user_doc is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found", ) return User(**user_doc) def require_role(required_role: UserRole): async def role_checker(current_user: User = Depends(get_current_user)) -> User: if current_user.role != required_role and current_user.role != UserRole.ADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions", ) return current_user return role_checker def require_roles(*required_roles: UserRole): async def roles_checker(current_user: User = Depends(get_current_user)) -> User: if current_user.role not in required_roles and current_user.role != UserRole.ADMIN: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions", ) return current_user return roles_checker async def get_current_user_optional( request: Request, db: AsyncIOMotorDatabase = Depends(get_database), ) -> Optional[User]: authorization: str = request.headers.get("Authorization") if not authorization: return None try: scheme, token = authorization.split() if scheme.lower() != "bearer": return None payload = decode_token(token) user_id: str = payload.get("sub") if user_id is None: return None user_doc = await db.users.find_one({"_id": user_id}) if user_doc is None: return None return User(**user_doc) except Exception: return None async def get_accessible_project_ids( user: User, db: AsyncIOMotorDatabase, ) -> Optional[list[str]]: """ Returns project IDs the user may access, or None meaning "see everything". - Staff / Admin → None (unrestricted) - Otherwise → projects in orgs where the user holds any membership (falls back to legacy pm_client_ids/team lookups if no memberships found) """ if user.role in STAFF_ROLES: return None # Primary path: use memberships collection (Phase 3 SaaS) user_id = str(user.id) membership_cursor = db.memberships.find({"user_id": user_id}, {"organization_id": 1}) org_ids = [doc["organization_id"] async for doc in membership_cursor] if org_ids: projects = await db.projects.find( {"client_id": {"$in": org_ids}, "is_active": True}, {"_id": 1}, ).to_list(None) return [str(p["_id"]) for p in projects] # Legacy fallback (pre-backfill) — keeps the app working before migration runs if user.role == UserRole.PROJECT_MANAGER: client_ids = user.pm_client_ids or [] if not client_ids: return [] projects = await db.projects.find( {"client_id": {"$in": client_ids}, "is_active": True}, {"_id": 1}, ).to_list(None) return [str(p["_id"]) for p in projects] teams = await db.teams.find( {"member_user_ids": user_id}, {"client_id": 1}, ).to_list(None) client_ids = list({t["client_id"] for t in teams}) if not client_ids: return [] projects = await db.projects.find( {"client_id": {"$in": client_ids}, "is_active": True}, {"_id": 1}, ).to_list(None) return [str(p["_id"]) for p in projects] def require_pm_for_client(client_id_param: str = "client_id"): """Dependency: ensures the current user is an Admin or PM for the given client.""" async def checker( request: Request, current_user: User = Depends(get_current_user), ) -> User: if current_user.role == UserRole.ADMIN: return current_user if current_user.role != UserRole.PROJECT_MANAGER: raise HTTPException(status_code=403, detail="Insufficient permissions") client_id = request.path_params.get(client_id_param) if client_id not in (current_user.pm_client_ids or []): raise HTTPException(status_code=403, detail="Not a manager for this client") return current_user return checker