feat: implement sidebar components and authentication flow

- Added sidebar menu components including sidebar-menu-item, sidebar-menu-skeleton, sidebar-menu-sub-button, sidebar-menu-sub-item, sidebar-menu-sub, sidebar-menu, sidebar-provider, sidebar-rail, sidebar-separator, sidebar-trigger, and sidebar.
- Introduced skeleton loading states for sidebar items.
- Integrated tooltip components for enhanced user interaction.
- Developed mobile responsiveness using media queries.
- Established authentication flow with Keycloak, including login, logout, and token management.
- Implemented server-side redirection based on authentication status.
- Enhanced error handling and logging for authentication processes.
This commit is contained in:
2025-11-01 20:04:43 -05:00
parent 7adc90ac33
commit 73a535291a
140 changed files with 4304 additions and 498 deletions

View File

@@ -125,3 +125,17 @@ class ExchangeCodeRequestDTO(BaseModel):
"tenant_slug": "empresa-abc"
}
}
class SetCookieRequestDTO(BaseModel):
"""DTO para establecer cookies de autenticación"""
access_token: str = Field(..., description="Access token JWT")
refresh_token: str = Field(..., description="Refresh token JWT")
class Config:
json_schema_extra = {
"example": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}

View File

@@ -1,7 +1,7 @@
"""
Endpoints API para autenticación
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
@@ -15,7 +15,8 @@ from .dto import (
LogoutRequestDTO,
RegisterRequestDTO,
RegisterResponseDTO,
ExchangeCodeRequestDTO
ExchangeCodeRequestDTO,
SetCookieRequestDTO
)
from .service import AuthService
@@ -117,4 +118,65 @@ async def exchange_code(
externo y Keycloak lo redirige al frontend con el código en los query params.
"""
service = AuthService(db)
return service.exchange_code(exchange_data)
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)}"
)

View File

@@ -59,15 +59,21 @@ class TenantMiddleware(BaseHTTPMiddleware):
user_info = verify_token(token)
tenant_id = get_tenant_from_token(user_info)
# ⚠️ NOTA: tenant_id puede ser None para usuarios SSO que aún no tienen tenant asignado
# En ese caso, el endpoint específico deberá manejarlo
if not tenant_id:
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
logger.warning(f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}")
# No lanzamos error aquí, dejamos que el endpoint decida qué hacer
# Agregar tenant_id al state del request
# Agregar tenant_id al state del request (puede ser None)
request.state.tenant_id = tenant_id
request.state.user_info = user_info
except HTTPException:
# Re-lanzar HTTPException directamente
raise
except Exception as e:
logger.error(f"Tenant validation error: {str(e)}")
logger.error(f"Tenant validation error: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid authentication")
response = await call_next(request)