"""API Key authentication dependency for FastAPI.""" from datetime import timedelta from fastapi import Depends, HTTPException, Request, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.backend.database import get_db from src.backend.config import settings from src.backend.models.orm.api_key import ApiKey from src.backend.models.orm.user import User from src.backend.services.auth_service import hash_api_key from src.backend.utils.tempo import utcnow _RENEW_INTERVAL = timedelta(minutes=5) async def get_current_user( request: Request, db: AsyncSession = Depends(get_db), ) -> User: """Extract API key from header and return the authenticated user. The API key is sent in the X-API-Key header on every request. Login endpoint is excluded from this check. """ api_key = request.headers.get("X-API-Key") if not api_key: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing API key in X-API-Key header", ) result = await db.execute( select(ApiKey, User) .join(User, User.id == ApiKey.user_id) .where(ApiKey.key_hash == hash_api_key(api_key), User.active == True) ) row = result.first() now = utcnow() if row is None or row[0].expires_at <= now: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired API key", ) key_row, user = row # Sliding TTL, written at most once per _RENEW_INTERVAL to spare the DB. if key_row.last_used_at is None or now - key_row.last_used_at > _RENEW_INTERVAL: key_row.last_used_at = now key_row.expires_at = now + timedelta(hours=settings.api_key_ttl_hours) await db.flush() # Store user_id in request state for access logging middleware request.state.user_id = user.id return user def require_role(role: str): """Dependency factory that checks if user has a specific role.""" async def check_role(user: User = Depends(get_current_user)) -> User: if not user.has_role(role) and not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Role '{role}' required", ) return user return check_role def require_admin(): """Dependency that checks if user is admin.""" async def check_admin(user: User = Depends(get_current_user)) -> User: if not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required", ) return user return check_admin # Pre-built role dependencies for convenience require_maker = require_role("Maker") require_measurement_tec = require_role("MeasurementTec") require_metrologist = require_role("Metrologist") require_supervisor = require_role("Supervisor") require_admin_user = require_admin()