feat: Implement multi-tenancy support in middleware and security layers

- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
This commit is contained in:
2025-11-11 14:00:56 -06:00
parent e1eb6bbd01
commit 52b8fcd434
242 changed files with 7067 additions and 3274 deletions

View File

@@ -1,6 +1,7 @@
"""
Endpoints API para autenticación
"""
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
@@ -16,7 +17,7 @@ from .dto import (
RegisterRequestDTO,
RegisterResponseDTO,
ExchangeCodeRequestDTO,
SetCookieRequestDTO
SetCookieRequestDTO,
)
from .service import AuthService
@@ -26,12 +27,11 @@ security = HTTPBearer()
@router.post("/register", response_model=RegisterResponseDTO, status_code=201)
async def register(
register_data: RegisterRequestDTO,
db: Session = Depends(get_core_db)
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
@@ -39,7 +39,7 @@ async def register(
- 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
@@ -50,13 +50,10 @@ async def register(
@router.post("/login", response_model=TokenResponseDTO)
async def login(
login_data: LoginRequestDTO,
db: Session = Depends(get_core_db)
):
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
@@ -68,8 +65,7 @@ async def login(
@router.post("/refresh", response_model=TokenResponseDTO)
async def refresh_token(
refresh_data: RefreshTokenRequestDTO,
db: Session = Depends(get_core_db)
refresh_data: RefreshTokenRequestDTO, db: Session = Depends(get_core_db)
):
"""
Refresca el access token usando el refresh token
@@ -81,7 +77,7 @@ async def refresh_token(
@router.get("/me", response_model=UserInfoResponseDTO)
async def get_current_user_info(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_core_db)
db: Session = Depends(get_core_db),
):
"""
Obtiene información del usuario actual desde el token
@@ -94,7 +90,7 @@ async def get_current_user_info(
async def logout(
logout_data: LogoutRequestDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Cierra sesión invalidando el refresh token
@@ -105,15 +101,14 @@ async def logout(
@router.post("/exchange-code", response_model=TokenResponseDTO)
async def exchange_code(
exchange_data: ExchangeCodeRequestDTO,
db: Session = Depends(get_core_db)
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.
"""
@@ -125,15 +120,15 @@ async def exchange_code(
async def set_cookie(
cookie_data: SetCookieRequestDTO,
response: Response,
db: Session = Depends(get_core_db)
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)
@@ -145,7 +140,7 @@ async def set_cookie(
try:
# Validar el access token
user_info = service.get_user_info(cookie_data.access_token)
# Establecer las cookies
# Access token cookie
response.set_cookie(
@@ -155,9 +150,9 @@ async def set_cookie(
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="/"
path="/",
)
# Refresh token cookie
response.set_cookie(
key="refresh_token",
@@ -166,17 +161,14 @@ async def set_cookie(
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="/"
path="/",
)
return {
"success": True,
"message": "Cookies establecidas correctamente",
"user": user_info
"user": user_info,
}
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Error validando tokens: {str(e)}"
)
raise HTTPException(status_code=400, detail=f"Error validando tokens: {str(e)}")