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