from typing import Optional from fastapi import Depends, HTTPException, status from starlette.requests import Request from fastapi.security import OAuth2PasswordBearer from jose import jwt, JWTError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import ValidationError from app.core.database import get_db from app.core.security import security from app.core.config import get_settings from app.models.user import User, UserRole from app.models.tenant import Tenant settings = get_settings() # Esquema OAuth2 centralizado — auth.py importa desde aquí # Soporta: 1) Authorization: Bearer header (Swagger/API clients) # 2) Cookie access_token HttpOnly (apps web) _bearer_scheme = OAuth2PasswordBearer( tokenUrl=f"/{settings.API_VERSION}/auth/login", auto_error=False, ) async def oauth2_scheme( request: Request, bearer_token: Optional[str] = Depends(_bearer_scheme), ) -> str: """Extrae JWT desde header Authorization (prioridad) o cookie del frontend correcto. Usa el header X-App para seleccionar la cookie: - X-App: internal → solo 'internal_access_token' - X-App: client → solo 'client_access_token' - sin header → prueba ambas (compatibilidad con Swagger/CLI) """ if bearer_token: return bearer_token app_hint = request.headers.get("X-App", "").lower() if app_hint == "internal": token = request.cookies.get("internal_access_token") elif app_hint == "client": token = request.cookies.get("client_access_token") else: # Fallback para Swagger, tests y clientes sin header token = ( request.cookies.get("internal_access_token") or request.cookies.get("client_access_token") ) if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) return token async def get_current_user( request: Request, token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db) ) -> User: payload = security.verify_token(token) if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) user_id: str = payload.get("sub") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) result = await db.execute(select(User).where(User.id == user_id)) user = result.scalars().first() if user is None: raise HTTPException(status_code=404, detail="User not found") if not user.is_active: raise HTTPException(status_code=400, detail="Inactive user") # Enforce that tenant header (if present) matches the authenticated user's tenant. # Roles globales (is_global) pueden operar en cualquier tenant → omitir chequeo. # Roles de cliente (is_client) deben coincidir con su propio tenant. request_tenant_id = getattr(getattr(request, "state", None), "tenant_id", None) if request_tenant_id and not user.role.is_global and str(user.tenant_id) != str(request_tenant_id): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Tenant header does not match authenticated user", ) return user async def get_current_active_superuser( current_user: User = Depends(get_current_user), ) -> User: if current_user.role != UserRole.ADMIN: raise HTTPException( status_code=403, detail="The user doesn't have enough privileges" ) return current_user async def get_current_tenant( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ) -> Tenant: """Obtener el tenant del usuario actual.""" result = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id)) tenant = result.scalar_one_or_none() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found" ) return tenant