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

@@ -5,18 +5,18 @@ Core module - Configuración y utilidades centrales de la aplicación
from .config import settings
from .database import (
Base,
get_core_db,
get_async_core_db,
get_core_db,
get_tenant_db,
init_db,
init_async_db,
init_db,
)
from .security import (
verify_token,
get_current_user,
get_current_active_user,
has_role,
get_current_user,
get_tenant_from_token,
has_role,
verify_token,
)
__all__ = [

View File

@@ -2,9 +2,10 @@
Configuración centralizada de la aplicación usando Pydantic Settings
"""
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Configuración de la aplicación"""

View File

@@ -4,12 +4,13 @@ Configuración de base de datos con soporte multi-tenant
- Bases de datos dedicadas para clientes enterprise
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from typing import Generator, Dict, Optional, AsyncGenerator
from contextlib import contextmanager
from typing import AsyncGenerator, Dict, Generator, Optional
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from .config import settings
# Base declarativa para modelos ORM
@@ -21,7 +22,7 @@ core_engine = create_engine(
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
echo=settings.DEBUG,
echo=False,
)
CoreSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=core_engine)

View File

@@ -5,16 +5,16 @@ Middleware personalizado para Anexo76
- Logging de requests
"""
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Callable
import logging
import time
from datetime import datetime
from sqlalchemy.orm import Session
from .database import CoreSessionLocal
from .security import verify_token, get_tenant_from_token
from typing import Callable
from fastapi import HTTPException, Request
from starlette.middleware.base import BaseHTTPMiddleware
from .config import settings
from .database import CoreSessionLocal
from .security import get_tenant_from_token, verify_token
logger = logging.getLogger(__name__)
@@ -158,6 +158,19 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: Callable):
start_time = time.time()
excluded_paths = [
"/api/docs",
"/api/redoc",
"/openapi.json",
"/api/v1/status",
"/api/health",
]
if any(
request.url.path == path or request.url.path.startswith(path + "/")
for path in excluded_paths
):
return await call_next(request)
# Log request
logger.info(f"Request: {request.method} {request.url.path}")

View File

@@ -2,15 +2,16 @@
Utilidades de seguridad y autenticación con Keycloak
"""
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from keycloak import KeycloakOpenID
from jose import jwt, JWTError
from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
from .config import settings
import logging
from core.database import get_core_db
from typing import Any, Dict, Optional
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from keycloak import KeycloakOpenID
from sqlalchemy.orm import Session
from .config import settings
logger = logging.getLogger(__name__)
@@ -74,7 +75,7 @@ async def get_current_user(
current_user: dict = Depends(get_current_user)
"""
token = credentials.credentials
user_info = verify_token(token)
user_info = verify_token(token)
return user_info
@@ -125,7 +126,7 @@ def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]:
- En atributos del usuario
"""
# Intentar obtener de claims personalizados
tenant_id = user_info.get("tenant_id")
tenant_id = user_info.get("tenant_id")
if not tenant_id:
# Intentar obtener de atributos
tenant_id = user_info.get("attributes", {}).get("tenant_id")
@@ -136,7 +137,9 @@ def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]:
return None
def validate_company_access(db: Session, company_id: int, current_user: Dict[str, Any]) -> bool:
def validate_company_access(
db: Session, company_id: int, current_user: Dict[str, Any]
) -> bool:
"""
Valida que el usuario tenga acceso a la compañía solicitada
@@ -150,7 +153,7 @@ def validate_company_access(db: Session, company_id: int, current_user: Dict[str
Nota:
Verifica que la compañía pertenezca al tenant del usuario consultando la BD.
"""
tenant_id = get_tenant_from_token(current_user)
# Si no hay tenant_id en el token, denegar acceso
@@ -160,18 +163,21 @@ def validate_company_access(db: Session, company_id: int, current_user: Dict[str
# Consultar si la compañía pertenece al tenant
try:
from api.v1.modules.a76.company.models import Company
company = db.query(Company).filter(
Company.id == company_id,
Company.tenant_id == tenant_id
).first()
company = (
db.query(Company)
.filter(Company.id == company_id, Company.tenant_id == tenant_id)
.first()
)
return company is not None
finally:
db.close()
def validate_access_to_resource(db: Session, company_id: int, current_user: Dict[str, Any]) -> int:
def validate_access_to_resource(
db: Session, company_id: int, current_user: Dict[str, Any]
) -> int:
"""
Valida que el usuario tenga acceso a un recurso específico basado en company_id
y regresa el tenant_id
@@ -196,23 +202,3 @@ def validate_access_to_resource(db: Session, company_id: int, current_user: Dict
# Validar que el tenant_id del usuario coincida con el del recurso
return tenant_id
class KeycloakClient:
"""Cliente para interactuar con Keycloak Admin API"""
def __init__(self):
self.openid = keycloak_openid
def create_user(self, email: str, password: str, tenant_id: int, **kwargs):
"""Crea un usuario en Keycloak"""
# Implementar lógica para crear usuario usando keycloak admin
pass
def assign_role(self, user_id: str, role: str):
"""Asigna un rol a un usuario"""
pass
def create_tenant_realm(self, tenant_name: str):
"""Crea un realm para un nuevo tenant"""
pass