""" FastAPI dependencies for authentication and user-scoped data access. """ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.auth import decode_token from app.database import get_db from app.models.user import User from app.models.photos import Photo from app.models.folders import Folder, SourceRoot from app.models.heaps import Heap from app.models.tags import Tag oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") async def get_current_user( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db), ) -> User: """Decode JWT, look up user, raise 401 if invalid or inactive.""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = decode_token(token) user_id: str = payload.get("sub") token_type: str = payload.get("type") if user_id is None or token_type != "access": raise credentials_exception except JWTError: raise credentials_exception result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if user is None or not user.is_active: raise credentials_exception return user async def require_admin( user: User = Depends(get_current_user), ) -> User: """Raise 403 if user is not an admin.""" if user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) return user # --------------------------------------------------------------------------- # User-scoped query helpers # --------------------------------------------------------------------------- def user_photos_query(user: User): """Base select for photos owned by user, with tags eager-loaded.""" return ( select(Photo) .options(selectinload(Photo.tags)) .where(Photo.user_id == user.id) ) async def get_user_photo( photo_id: str, user: User, db: AsyncSession, ) -> Photo: """Fetch a single photo by ID, scoped to the user. Raises 404.""" result = await db.execute( select(Photo) .options(selectinload(Photo.tags)) .where(Photo.id == photo_id, Photo.user_id == user.id) ) photo = result.scalar_one_or_none() if photo is None: raise HTTPException(status_code=404, detail="Photo not found") return photo async def get_user_folder( folder_id: str, user: User, db: AsyncSession, ) -> Folder: """Fetch a single folder by ID, scoped to the user. Raises 404.""" result = await db.execute( select(Folder).where(Folder.id == folder_id, Folder.user_id == user.id) ) folder = result.scalar_one_or_none() if folder is None: raise HTTPException(status_code=404, detail="Folder not found") return folder async def get_user_heap( heap_id: str, user: User, db: AsyncSession, ) -> Heap: """Fetch a single heap by ID, scoped to the user. Raises 404.""" result = await db.execute( select(Heap).where(Heap.id == heap_id, Heap.user_id == user.id) ) heap = result.scalar_one_or_none() if heap is None: raise HTTPException(status_code=404, detail="Heap not found") return heap async def get_user_tag( tag_id: str, user: User, db: AsyncSession, ) -> Tag: """Fetch a single tag by ID, scoped to the user. Raises 404.""" result = await db.execute( select(Tag).where(Tag.id == tag_id, Tag.user_id == user.id) ) tag = result.scalar_one_or_none() if tag is None: raise HTTPException(status_code=404, detail="Tag not found") return tag