""" Endpoints API para autenticación """ from core.database import get_core_db from core.security import get_current_user from fastapi import APIRouter, Depends, HTTPException, Response from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session from .dto import ( ExchangeCodeRequestDTO, LoginRequestDTO, LogoutRequestDTO, RefreshTokenRequestDTO, RegisterRequestDTO, RegisterResponseDTO, SetCookieRequestDTO, TokenResponseDTO, UserInfoResponseDTO, ) from .service import AuthService router = APIRouter(prefix="/auth", tags=["Authentication"]) security = HTTPBearer() @router.post("/register", response_model=RegisterResponseDTO, status_code=201) async def register( register_data: RegisterRequestDTO, db: Session = Depends(get_core_db) ): """ Registra un nuevo usuario en Keycloak El usuario debe proporcionar: - username: Nombre de usuario único - email: Email único - password: Contraseña (mínimo 8 caracteres) - first_name: Nombre - last_name: Apellido - tenant_slug: Slug del tenant al que pertenece El usuario se crea automáticamente en Keycloak con: - Cuenta habilitada - Rol 'user' asignado por defecto - Atributos de tenant """ service = AuthService(db) return service.register(register_data) @router.post("/login", response_model=TokenResponseDTO) async def login(login_data: LoginRequestDTO, db: Session = Depends(get_core_db)): """ Autentica usuario con Keycloak y retorna tokens JWT El usuario debe proporcionar: - username: Usuario o email - password: Contraseña - tenant_slug: Slug del tenant al que pertenece """ service = AuthService(db) return service.login(login_data) @router.post("/refresh", response_model=TokenResponseDTO) async def refresh_token( refresh_data: RefreshTokenRequestDTO, db: Session = Depends(get_core_db) ): """ Refresca el access token usando el refresh token """ service = AuthService(db) return service.refresh_token(refresh_data) @router.get("/me", response_model=UserInfoResponseDTO) async def get_current_user_info( credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_core_db), ): """ Obtiene información del usuario actual desde el token """ service = AuthService(db) return service.get_user_info(credentials.credentials) @router.post("/logout") async def logout( logout_data: LogoutRequestDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """ Cierra sesión invalidando el refresh token """ service = AuthService(db) return service.logout(logout_data) @router.post("/exchange-code", response_model=TokenResponseDTO) async def exchange_code( exchange_data: ExchangeCodeRequestDTO, db: Session = Depends(get_core_db) ): """ Intercambia un authorization code de OAuth2 por tokens Este endpoint es útil cuando el frontend usa el flujo de autorización con proveedores externos (Microsoft, Google, etc.) a través de Keycloak. El código se obtiene después de que el usuario se autentica con el proveedor externo y Keycloak lo redirige al frontend con el código en los query params. """ service = AuthService(db) return service.exchange_code(exchange_data) @router.post("/set-cookie") async def set_cookie( cookie_data: SetCookieRequestDTO, response: Response, db: Session = Depends(get_core_db), ): """ Establece cookies HttpOnly con los tokens de autenticación Este endpoint se llama desde el frontend después de una autenticación SSO exitosa para establecer las cookies de sesión necesarias para la validación server-side en los layouts protegidos. Las cookies se configuran como: - HttpOnly: No accesibles desde JavaScript (mayor seguridad) - Secure: Solo se envían por HTTPS (en producción) - SameSite=Lax: Protección contra CSRF - Max-Age: Tiempo de vida del token """ # Validar que los tokens sean válidos decodificándolos service = AuthService(db) try: # Validar el access token user_info = service.get_user_info(cookie_data.access_token) # Establecer las cookies # Access token cookie response.set_cookie( key="access_token", value=cookie_data.access_token, httponly=True, # No accesible desde JavaScript secure=False, # TODO: Cambiar a True en producción con HTTPS samesite="lax", # Protección CSRF max_age=3600, # 1 hora (ajustar según configuración del token) path="/", ) # Refresh token cookie response.set_cookie( key="refresh_token", value=cookie_data.refresh_token, httponly=True, secure=False, # TODO: Cambiar a True en producción con HTTPS samesite="lax", max_age=86400, # 24 horas (ajustar según configuración del token) path="/", ) return { "success": True, "message": "Cookies establecidas correctamente", "user": user_info, } except Exception as e: raise HTTPException(status_code=400, detail=f"Error validando tokens: {str(e)}")