- Rearranged imports in multiple files for consistency and clarity. - Updated logging middleware to exclude specific paths from logging. - Enhanced security module by cleaning up token handling and improving tenant validation. - Added tenant and company scoped mixins for better database model management. - Implemented generic CRUD routes for tenant-scoped resources. - Improved error handling and response management in API routes. - Cleaned up login and logout processes to ensure proper session management. - Introduced mechanisms to clear local storage and cookies on tenant change. - Enhanced company store to detect tenant changes and clear data accordingly. - Added new DTO mixins for currency and value affect flags.
175 lines
5.3 KiB
Python
175 lines
5.3 KiB
Python
"""
|
|
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)}")
|