Files
plantillas-proyectos/backend/api/v1/modules/a76/auth/service.py
acazares 2a10d7d267 feat: Add frontend and backend initialization scripts, implement Keycloak and PostgreSQL setup
- Implemented SvelteKit frontend with authentication callback handling.
- Created demo routes and paraglide localization functionality.
- Added health check and entrypoint scripts for backend services.
- Established PostgreSQL and Keycloak initialization scripts with health checks.
- Introduced models for database schema using SQLAlchemy.
- Configured Vite and SvelteKit for development and testing environments.
- Added health check script to verify service statuses and resource usage.
- Created Docker entrypoint scripts for seamless service startup.
2025-10-19 00:14:06 -05:00

176 lines
6.2 KiB
Python

"""
Servicio de autenticación con Keycloak
"""
from keycloak import KeycloakOpenID, KeycloakAdmin
from keycloak.exceptions import KeycloakError
from fastapi import HTTPException
from sqlalchemy.orm import Session
import logging
from core.config import settings
from .dto import (
LoginRequestDTO,
TokenResponseDTO,
RefreshTokenRequestDTO,
UserInfoResponseDTO,
LogoutRequestDTO
)
logger = logging.getLogger(__name__)
class AuthService:
"""Servicio de autenticación"""
def __init__(self, db: Session):
self.db = db
self.keycloak_openid = KeycloakOpenID(
server_url=settings.KEYCLOAK_SERVER_URL,
client_id=settings.KEYCLOAK_CLIENT_ID,
realm_name=settings.KEYCLOAK_REALM,
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET
)
def login(self, login_data: LoginRequestDTO) -> TokenResponseDTO:
"""
Autentica usuario y obtiene tokens
Args:
login_data: Credenciales de login
Returns:
TokenResponseDTO con access_token y refresh_token
Raises:
HTTPException: Si las credenciales son inválidas
"""
try:
# Verificar que el tenant existe
from api.v1.modules.a76.tenants.service import TenantService
tenant_service = TenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug)
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
if not tenant.is_active:
raise HTTPException(status_code=403, detail="Tenant is not active")
# Cambiar realm al del tenant
self.keycloak_openid.realm_name = tenant.keycloak_realm
# Obtener token de Keycloak
token_response = self.keycloak_openid.token(
username=login_data.username,
password=login_data.password
)
logger.info(f"User logged in: {login_data.username} (tenant: {tenant.slug})")
return TokenResponseDTO(
access_token=token_response["access_token"],
refresh_token=token_response["refresh_token"],
token_type="bearer",
expires_in=token_response["expires_in"]
)
except KeycloakError as e:
logger.warning(f"Keycloak authentication failed: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid credentials")
except HTTPException:
raise
except Exception as e:
logger.error(f"Login error: {str(e)}")
raise HTTPException(status_code=500, detail="Authentication error")
def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO:
"""
Refresca el access token usando refresh token
Args:
refresh_data: Refresh token
Returns:
TokenResponseDTO con nuevos tokens
"""
try:
token_response = self.keycloak_openid.refresh_token(
refresh_data.refresh_token
)
return TokenResponseDTO(
access_token=token_response["access_token"],
refresh_token=token_response["refresh_token"],
token_type="bearer",
expires_in=token_response["expires_in"]
)
except KeycloakError as e:
logger.warning(f"Token refresh failed: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
except Exception as e:
logger.error(f"Token refresh error: {str(e)}")
raise HTTPException(status_code=500, detail="Token refresh error")
def get_user_info(self, access_token: str) -> UserInfoResponseDTO:
"""
Obtiene información del usuario desde el token
Args:
access_token: Access token JWT
Returns:
UserInfoResponseDTO con información del usuario
"""
try:
user_info = self.keycloak_openid.userinfo(access_token)
# Extraer roles
roles = []
if "realm_access" in user_info:
roles = user_info["realm_access"].get("roles", [])
# Extraer tenant_id si está presente
tenant_id = user_info.get("tenant_id")
if not tenant_id and "attributes" in user_info:
tenant_id = user_info["attributes"].get("tenant_id")
return UserInfoResponseDTO(
sub=user_info.get("sub"),
email=user_info.get("email"),
name=user_info.get("name"),
preferred_username=user_info.get("preferred_username"),
tenant_id=int(tenant_id) if tenant_id else None,
roles=roles
)
except KeycloakError as e:
logger.warning(f"Get user info failed: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid token")
except Exception as e:
logger.error(f"Get user info error: {str(e)}")
raise HTTPException(status_code=500, detail="Error retrieving user info")
def logout(self, logout_data: LogoutRequestDTO) -> dict:
"""
Cierra sesión invalidando el refresh token
Args:
logout_data: Refresh token a invalidar
Returns:
Dict con mensaje de éxito
"""
try:
self.keycloak_openid.logout(logout_data.refresh_token)
logger.info("User logged out successfully")
return {"message": "Logged out successfully"}
except KeycloakError as e:
logger.warning(f"Logout failed: {str(e)}")
# No lanzamos error aquí, el logout puede fallar si el token ya expiró
return {"message": "Logged out"}
except Exception as e:
logger.error(f"Logout error: {str(e)}")
raise HTTPException(status_code=500, detail="Logout error")