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:
@@ -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..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user