Refactor backend and frontend code for improved structure and functionality

- 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.
This commit is contained in:
2025-11-11 17:20:47 -06:00
parent ac7f8d19d8
commit b68c4316ff
247 changed files with 2248 additions and 2504 deletions

View File

@@ -2,9 +2,10 @@
DTOs para módulo de autenticación
"""
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class LoginRequestDTO(BaseModel):
"""DTO para solicitud de login"""

View File

@@ -2,22 +2,22 @@
Endpoints API para autenticación
"""
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
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,
TokenResponseDTO,
RefreshTokenRequestDTO,
UserInfoResponseDTO,
LogoutRequestDTO,
RefreshTokenRequestDTO,
RegisterRequestDTO,
RegisterResponseDTO,
ExchangeCodeRequestDTO,
SetCookieRequestDTO,
TokenResponseDTO,
UserInfoResponseDTO,
)
from .service import AuthService

View File

@@ -2,21 +2,24 @@
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 api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.a76.user_tenant.service import UserTenantService
from core.config import settings
from fastapi import HTTPException
from keycloak import KeycloakAdmin, KeycloakOpenID
from keycloak.exceptions import KeycloakError
from sqlalchemy.orm import Session
from .dto import (
LoginRequestDTO,
TokenResponseDTO,
RefreshTokenRequestDTO,
UserInfoResponseDTO,
LogoutRequestDTO,
RefreshTokenRequestDTO,
RegisterRequestDTO,
RegisterResponseDTO,
TokenResponseDTO,
UserInfoResponseDTO,
)
logger = logging.getLogger(__name__)
@@ -49,9 +52,6 @@ class AuthService:
"""
try:
# Verificar que el tenant existe
from api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.a76.user_tenant.service import UserTenantService
tenant_service = TenantService(self.db)
user_tenant_service = UserTenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug)
@@ -70,54 +70,51 @@ class AuthService:
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
)
# Obtener token de Keycloak
token_response = keycloak_client.token(
username=login_data.username,
password=login_data.password,
grant_type=["password"],
)
# PASO 1: Primero actualizamos los atributos del usuario ANTES de autenticar
# Esto es necesario para que los Protocol Mappers incluyan los valores correctos
# en el token que se generará a continuación
# Obtener información del usuario y verificar acceso al tenant
user_info = keycloak_client.userinfo(token_response["access_token"])
user_id = user_info.get("sub")
if user_id:
# Verificar si el usuario tiene acceso a este tenant
has_access = user_tenant_service.user_has_access_to_tenant(
user_id, tenant.id
# Para obtener el user_id, necesitamos hacer una autenticación temporal
# o buscar el usuario por username
try:
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,
user_realm_name="master",
verify=True,
)
if not has_access:
logger.warning(
f"User {user_id} tried to access tenant {tenant.id} without permission"
)
raise HTTPException(
status_code=403, detail="You don't have access to this tenant"
# Buscar usuario por username
users = keycloak_admin.get_users({"username": login_data.username})
if users and len(users) > 0:
user_id = users[0]["id"]
# Verificar si el usuario tiene acceso a este tenant
has_access = user_tenant_service.user_has_access_to_tenant(
user_id, tenant.id
)
# Actualizar el tenant_id del usuario en Keycloak basado en el slug usado
try:
# Crear instancia de KeycloakAdmin para actualizar atributos
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,
user_realm_name="master",
verify=True,
)
if not has_access:
logger.warning(
f"User {user_id} tried to access tenant {tenant.id} without permission"
)
raise HTTPException(
status_code=403,
detail="You don't have access to this tenant",
)
# Obtener los datos actuales del usuario para no sobrescribirlos
# Obtener los datos actuales del usuario
current_user = keycloak_admin.get_user(user_id)
# Obtener los atributos actuales o crear un dict vacío
current_attributes = current_user.get("attributes", {})
# Actualizar solo los atributos de tenant
# Actualizar los atributos de tenant
current_attributes["tenant_id"] = [str(tenant.id)]
current_attributes["tenant_slug"] = [tenant.slug]
# Actualizar el usuario enviando TODOS los campos para evitar que se borren
# Actualizar el usuario con los nuevos atributos
update_payload = {
"email": current_user.get("email"),
"firstName": current_user.get("firstName"),
@@ -128,13 +125,23 @@ class AuthService:
}
keycloak_admin.update_user(user_id=user_id, payload=update_payload)
logger.info(
f"Updated tenant_id={tenant.id} for user {login_data.username}"
)
except Exception as e:
# No queremos que falle el login si no se puede actualizar el atributo
logger.warning(f"Could not update tenant_id attribute: {str(e)}")
except KeycloakError as e:
logger.warning(f"Could not pre-update user attributes: {str(e)}")
# Continuamos con el login aunque falle la actualización
except HTTPException:
raise # Re-lanzamos las excepciones HTTP (como acceso denegado)
except Exception as e:
logger.warning(f"Error pre-updating user attributes: {str(e)}")
# PASO 2: Ahora autenticamos al usuario
# Si los Protocol Mappers están configurados, el token incluirá
# automáticamente los atributos tenant_id y tenant_slug actualizados
token_response = keycloak_client.token(
username=login_data.username,
password=login_data.password,
grant_type=["password"],
)
return TokenResponseDTO(
access_token=token_response["access_token"],
@@ -234,7 +241,6 @@ class AuthService:
"""
try:
self.keycloak_openid.logout(logout_data.refresh_token)
logger.info("User logged out successfully")
return {"message": "Logged out successfully"}
except KeycloakError as e:
@@ -307,7 +313,6 @@ class AuthService:
user_role = keycloak_admin.get_realm_role("user")
if user_role:
keycloak_admin.assign_realm_roles(user_id, [user_role])
logger.info(f"Assigned 'user' role to {register_data.username}")
except KeycloakError as e:
# El rol 'user' no existe, no es un error crítico
logger.warning(f"Could not assign 'user' role: {str(e)}")
@@ -322,23 +327,17 @@ class AuthService:
tenant_id=tenant.id,
role="user", # Rol por defecto
)
logger.info(f"Added user {user_id} to tenant {tenant.id} in database")
except Exception as e:
# Si falla, hacer rollback del usuario en Keycloak
logger.error(f"Failed to add user to tenant in database: {str(e)}")
try:
keycloak_admin.delete_user(user_id)
logger.info(f"Rolled back user creation in Keycloak")
except:
except Exception as e:
pass
raise HTTPException(
status_code=500, detail="Failed to register user in database"
)
logger.info(
f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})"
)
return RegisterResponseDTO(
user_id=user_id,
username=register_data.username,
@@ -385,7 +384,6 @@ class AuthService:
"""
try:
# Importar el DTO aquí para evitar referencias circulares
from .dto import ExchangeCodeRequestDTO
# Intercambiar código por tokens usando Keycloak
token_response = self.keycloak_openid.token(
@@ -394,20 +392,11 @@ class AuthService:
redirect_uri=exchange_data.redirect_uri,
)
logger.info(f"Code exchanged successfully")
# Si se proporciona tenant_slug, podríamos validar que el usuario pertenece a ese tenant
# Por ahora simplemente retornamos los tokens
if exchange_data.tenant_slug:
# Decodificar token para obtener tenant_id del usuario
user_info = self.keycloak_openid.introspect(
token_response["access_token"]
)
user_tenant_id = user_info.get("tenant_id")
# Validar que el tenant existe y está activo
from api.v1.modules.a76.tenants.service import TenantService
tenant_service = TenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(exchange_data.tenant_slug)