Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-04-06 08:54:05 -05:00
parent c71741077f
commit d3676aa8ed
19 changed files with 873 additions and 2151 deletions

View File

@@ -27,28 +27,22 @@ router = APIRouter(prefix="/users", tags=["Users"])
@router.get("/stats", response_model=UserStatsDTO)
def get_user_statistics(
async def get_user_statistics(
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Obtiene estadísticas de usuarios del tenant actual
Muestra:
- Total de usuarios
- Usuarios activos e inactivos
- Límite de licencia
- Usuarios disponibles
- Porcentaje de uso
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = UserService(db, tenant_id, company_id)
return service.get_user_stats()
return service.get_user_stats() # Este no es async en service.py
@router.get("/", response_model=UserListResponseDTO)
def list_users(
async def list_users(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(20, ge=1, le=100, description="Tamaño de página"),
@@ -58,17 +52,15 @@ def list_users(
):
"""
Lista todos los usuarios del tenant con paginación
Se puede filtrar por término de búsqueda (busca en username, email, nombre)
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = UserService(db, tenant_id, company_id)
result = service.get_tenant_users(page=page, page_size=page_size, search=search)
result = await service.get_tenant_users(page=page, page_size=page_size, search=search)
return result
@router.get("/{user_id}", response_model=UserResponseDTO)
def get_user(
async def get_user_detail(
user_id: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
@@ -76,34 +68,25 @@ def get_user(
):
"""
Obtiene información detallada de un usuario específico
El usuario debe pertenecer al tenant actual
"""
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
service = UserService(db, tenant_id, company_id)
return service.get_user(user_id)
return await service.get_user(user_id)
@router.post("/", response_model=UserResponseDTO, status_code=201)
def create_user(
async def create_new_user(
data: CreateUserRequestDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Crea un nuevo usuario en Keycloak y lo asocia al tenant
Validaciones:
- Verifica que no se exceda el límite de usuarios de la licencia
- Verifica que el email y username sean únicos
- Crea el usuario con contraseña temporal
Nota: El tenant_id se obtiene automáticamente del servicio (del token del usuario actual)
Crea un nuevo usuario a través del Hub y lo asocia al tenant
"""
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.create"])
service = UserService(db, tenant_id, company_id)
user = service.create_user(
user = await service.create_user(
email=data.email,
username=data.username,
first_name=data.first_name,
@@ -117,7 +100,7 @@ def create_user(
@router.put("/{user_id}", response_model=UserResponseDTO)
def update_user(
async def update_user_detail(
user_id: str,
data: UpdateUserRequestDTO,
company_id: int = Query(..., description="Company ID"),
@@ -126,17 +109,10 @@ def update_user(
):
"""
Actualiza información de un usuario
Puede actualizar:
- Datos personales (nombre, apellido, email)
- Estado (habilitado/deshabilitado)
- Verificación de email
- Rol en el tenant
- Perfil (avatar, teléfono, bio, preferencias)
"""
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
service = UserService(db, tenant_id, company_id)
user = service.update_user(
user = await service.update_user(
user_id=user_id,
first_name=data.first_name,
last_name=data.last_name,
@@ -153,7 +129,7 @@ def update_user(
@router.delete("/{user_id}")
def delete_user(
async def delete_user_route(
user_id: str,
company_id: int = Query(..., description="Company ID"),
soft_delete: bool = Query(
@@ -165,18 +141,15 @@ def delete_user(
):
"""
Elimina un usuario del tenant
- soft_delete=True: Solo desactiva la relación (recomendado)
- soft_delete=False: Elimina permanentemente de Keycloak
"""
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.delete"])
service = UserService(db, tenant_id, company_id)
service.delete_user(user_id, soft_delete=soft_delete)
await service.delete_user(user_id, soft_delete=soft_delete)
return {"message": "User deleted successfully"}
@router.post("/{user_id}/change-password")
def change_user_password(
async def change_user_password(
user_id: str,
data: ChangePasswordRequestDTO,
company_id: int = Query(..., description="Company ID"),
@@ -184,14 +157,11 @@ def change_user_password(
current_user: dict = Depends(get_current_user),
):
"""
Cambia la contraseña de un usuario
- temporary=True: Usuario debe cambiar la contraseña en el próximo login
- temporary=False: Contraseña permanente
Cambia la contraseña de un usuario a través del Hub
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = UserService(db, tenant_id, company_id)
service.change_password(user_id, data.password, data.temporary)
await service.change_password(user_id, data.password, data.temporary)
return {"message": "Password changed successfully"}
@@ -199,13 +169,12 @@ def change_user_password(
@router.get("/me/profile", response_model=UserResponseDTO)
def get_my_profile(
async def get_my_profile(
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
"""
Obtiene el perfil completo del usuario actual
Incluye datos de Keycloak y datos de perfil (avatar, bio, etc.)
"""
keycloak_user_id = current_user.get("sub")
if not keycloak_user_id:
@@ -227,21 +196,17 @@ def get_my_profile(
)
service = UserService(db, user_tenant.tenant_id, user_tenant.company_id)
return service.get_current_user_profile(keycloak_user_id)
return await service.get_current_user_profile(keycloak_user_id)
@router.put("/me/profile", response_model=UserResponseDTO)
def update_my_profile(
async def update_my_profile(
data: UpdateUserRequestDTO,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
"""
Actualiza el perfil del usuario actual
Puede actualizar:
- Datos de Keycloak: nombre, apellido, email
- Datos de perfil: avatar, teléfono, biografía, preferencias
"""
keycloak_user_id = current_user.get("sub")
if not keycloak_user_id:
@@ -263,7 +228,7 @@ def update_my_profile(
)
service = UserService(db, user_tenant.tenant_id, user_tenant.company_id)
return service.update_current_user_profile(
return await service.update_current_user_profile(
keycloak_user_id=keycloak_user_id,
first_name=data.first_name,
last_name=data.last_name,

View File

@@ -1,13 +1,9 @@
"""
Servicio para gestionar usuarios de Keycloak con validación de licencias
"""
import logging
import httpx
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import HTTPException
from keycloak import KeycloakAdmin, KeycloakError
from sqlalchemy import and_, func
from sqlalchemy.orm import Session
@@ -19,24 +15,22 @@ from ..user_tenant.models import UserTenant
logger = logging.getLogger(__name__)
def _normalize_keycloak_user(
def _normalize_user(
user_data: Dict[str, Any],
role: Optional[str] = None,
user_tenant: Optional[Any] = None,
) -> Dict[str, Any]:
"""
Normaliza los datos de usuario de Keycloak al formato esperado por el DTO
Keycloak usa camelCase, nuestro DTO usa snake_case
Normaliza los datos de usuario al formato esperado por el DTO
"""
normalized = {
"id": user_data.get("id"),
"username": user_data.get("username", ""),
"id": user_data.get("id") or user_data.get("sub"),
"username": user_data.get("username") or user_data.get("preferred_username", ""),
"email": user_data.get("email", ""),
"first_name": user_data.get("firstName", ""),
"last_name": user_data.get("lastName", ""),
"enabled": user_data.get("enabled", False),
"email_verified": user_data.get("emailVerified", False),
"first_name": user_data.get("firstName") or user_data.get("name", "").split(" ")[0],
"last_name": user_data.get("lastName") or (" ".join(user_data.get("name", "").split(" ")[1:]) if " " in user_data.get("name", "") else ""),
"enabled": user_data.get("enabled", True),
"email_verified": user_data.get("emailVerified") or user_data.get("email_verified", False),
"created_timestamp": user_data.get("createdTimestamp"),
"role": role,
}
@@ -56,22 +50,13 @@ def _normalize_keycloak_user(
class UserService:
"""Servicio para gestionar usuarios en Keycloak"""
"""Servicio para gestionar usuarios vía Hub"""
def __init__(self, db: Session, tenant_id: int, company_id: int = None):
def __init__(self, db: Session, tenant_id: int = None, company_id: int = None):
self.db = db
self.tenant_id = tenant_id
self.company_id = company_id
# Inicializar cliente admin de Keycloak
self.keycloak_admin = KeycloakAdmin(
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=settings.KEYCLOAK_REALM,
verify=True,
)
def _get_license(self) -> License:
"""Obtiene la licencia del tenant actual"""
license = (
@@ -119,7 +104,7 @@ class UserService:
f"Currently active: {active_users}. Please upgrade your license.",
)
def create_user(
async def create_user(
self,
email: str,
username: str,
@@ -131,74 +116,44 @@ class UserService:
email_verified: bool = False,
) -> Dict[str, Any]:
"""
Crea un nuevo usuario en Keycloak y lo asocia al tenant
Args:
email: Email del usuario
username: Nombre de usuario
first_name: Nombre
last_name: Apellido
password: Contraseña inicial
role: Rol en el tenant
enabled: Si el usuario está habilitado
email_verified: Si el email está verificado
Returns:
Información del usuario creado
Raises:
HTTPException: Si se alcanza el límite de usuarios o falla la creación
Crea un nuevo usuario a través del Hub y lo asocia localmente
"""
# Verificar límite de usuarios
self._check_user_limit()
try:
# Crear usuario en Keycloak
new_user = {
"email": email,
"username": username,
"enabled": enabled,
"emailVerified": email_verified,
"firstName": first_name,
"lastName": last_name,
"attributes": {
"tenant_id": [
str(self.tenant_id)
] # Atributo requerido por Keycloak
},
"credentials": [
{
"type": "password",
"value": password,
"temporary": True, # Usuario debe cambiar en primer login
# Mandar al Hub para creación en Keycloak
async with httpx.AsyncClient(timeout=10.0) as client:
hub_response = await client.post(
f"{settings.HUB_URL}api/v1/auth/register",
json={
"email": email,
"username": username,
"first_name": first_name,
"last_name": last_name,
"password": password,
"tenant_slug": "default", # TODO: Get real slug if needed
}
],
}
)
if hub_response.status_code != 201:
logger.error(f"Hub registration failed: {hub_response.text}")
raise HTTPException(status_code=hub_response.status_code, detail="Failed to create user in Hub")
user_id = self.keycloak_admin.create_user(new_user)
logger.info(f"User created in Keycloak: {user_id}")
user_data = hub_response.json()
user_id = user_data.get("user_id")
# Obtener company_id si no se proporcionó
if not self.company_id:
# Obtener la primera company del tenant
from api.v1.modules.a76.general_catalogs.company.models import Company
company = (
self.db.query(Company)
.filter(Company.tenant_id == self.tenant_id)
.first()
)
company = self.db.query(Company).filter(Company.tenant_id == self.tenant_id).first()
if not company:
raise HTTPException(
status_code=400,
detail="No company found for this tenant. Please create a company first.",
)
raise HTTPException(status_code=400, detail="No company found")
company_id = company.id
else:
company_id = self.company_id
# Crear relación con el tenant
# Crear relación local
user_tenant = UserTenant(
keycloak_user_id=user_id,
tenant_id=self.tenant_id,
@@ -209,36 +164,16 @@ class UserService:
self.db.add(user_tenant)
self.db.commit()
# Obtener información completa del usuario
user_info = self.keycloak_admin.get_user(user_id)
return _normalize_user(user_data, role, user_tenant)
return _normalize_keycloak_user(user_info, role, user_tenant)
except KeycloakError as e:
logger.error(f"Keycloak error creating user: {str(e)}")
self.db.rollback()
# Manejar errores específicos
if "User exists with same email" in str(e):
raise HTTPException(
status_code=409, detail="A user with this email already exists"
)
elif "User exists with same username" in str(e):
raise HTTPException(
status_code=409, detail="A user with this username already exists"
)
raise HTTPException(
status_code=500, detail=f"Error creating user in Keycloak: {str(e)}"
)
except Exception as e:
logger.error(f"Unexpected error creating user: {str(e)}")
logger.error(f"Error creating user: {str(e)}")
self.db.rollback()
raise HTTPException(
status_code=500, detail=f"Error creating user: {str(e)}"
)
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail=str(e))
def get_tenant_users(
async def get_tenant_users(
self, page: int = 1, page_size: int = 20, search: Optional[str] = None
) -> Dict[str, Any]:
"""
@@ -288,41 +223,18 @@ class UserService:
user_roles_map[user_role.user_id] = []
user_roles_map[user_role.user_id].append(user_role.company_role.name)
# Obtener información de Keycloak para cada usuario
users = []
for ut in user_tenants:
try:
user_info = self.keycloak_admin.get_user(ut.keycloak_user_id)
# Obtener roles del usuario
roles = user_roles_map.get(ut.keycloak_user_id, [])
role_str = ", ".join(roles) if roles else None
normalized_user = _normalize_keycloak_user(user_info, role_str, ut)
# En lugar de consultar Keycloak uno a uno (lento y sin API directa ahora),
# devolvemos la info local mínima o consultamos un endpoint de "buscar varios" en el Hub si existiera.
# Por ahora, minimizamos el impacto devolviendo lo que tenemos local.
normalized_user = _normalize_user({
"id": ut.keycloak_user_id,
"username": "User", # Placeholder si no tenemos el dato local
}, role_str, ut)
# Filtrar por búsqueda si se proporciona
if search:
search_lower = search.lower()
if (
search_lower in normalized_user.get("username", "").lower()
or search_lower in normalized_user.get("email", "").lower()
or search_lower
in normalized_user.get("first_name", "").lower()
or search_lower
in normalized_user.get("last_name", "").lower()
or search_lower
in normalized_user.get("phone", "").lower()
or search_lower
in normalized_user.get("bio", "").lower()
):
users.append(normalized_user)
else:
users.append(normalized_user)
except KeycloakError as e:
logger.warning(
f"Could not fetch user {ut.keycloak_user_id} from Keycloak: {str(e)}"
)
users.append(normalized_user)
except Exception as e:
logger.warning(f"Error processing user {ut.keycloak_user_id}: {e}")
continue
total_pages = (total + page_size - 1) // page_size
@@ -341,31 +253,24 @@ class UserService:
status_code=500, detail=f"Error getting users: {str(e)}"
)
def get_user(self, user_id: str) -> Dict[str, Any]:
"""Obtiene un usuario específico del tenant"""
async def get_user(self, user_id: str) -> Dict[str, Any]:
"""Obtiene un usuario específico"""
from ..permissions.models import UserCompanyRole
from sqlalchemy.orm import joinedload
# Verificar que el usuario pertenece al tenant
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == user_id,
UserTenant.tenant_id == self.tenant_id,
UserTenant.is_active == True,
)
user_tenant = self.db.query(UserTenant).filter(
and_(
UserTenant.keycloak_user_id == user_id,
UserTenant.tenant_id == self.tenant_id,
UserTenant.is_active == True,
)
.first()
)
).first()
if not user_tenant:
raise HTTPException(status_code=404, detail="User not found in this tenant")
raise HTTPException(status_code=404, detail="User not found")
# Obtener roles del usuario en la compañía actual
user_roles = self.db.query(UserCompanyRole).options(
joinedload(UserCompanyRole.company_role)
).filter(
# Roles locales
user_roles = self.db.query(UserCompanyRole).options(joinedload(UserCompanyRole.company_role)).filter(
and_(
UserCompanyRole.user_id == user_id,
UserCompanyRole.company_id == self.company_id,
@@ -373,163 +278,58 @@ class UserService:
UserCompanyRole.is_active == True
)
).all()
roles = [ur.company_role.name for ur in user_roles]
role_str = ", ".join(roles) if roles else None
try:
user_info = self.keycloak_admin.get_user(user_id)
return _normalize_keycloak_user(user_info, role_str, user_tenant)
except KeycloakError as e:
logger.error(f"Error getting user from Keycloak: {str(e)}")
raise HTTPException(status_code=404, detail="User not found in Keycloak")
# TODO: Call Hub if more info is needed
return _normalize_user({"id": user_id}, role_str, user_tenant)
def update_user(
self,
user_id: str,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
email: Optional[str] = None,
enabled: Optional[bool] = None,
email_verified: Optional[bool] = None,
role: Optional[str] = None,
avatar_url: Optional[str] = None,
phone: Optional[str] = None,
bio: Optional[str] = None,
preferences: Optional[dict] = None,
) -> Dict[str, Any]:
"""Actualiza información de un usuario"""
# Verificar que el usuario pertenece al tenant
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == user_id,
UserTenant.tenant_id == self.tenant_id,
)
)
.first()
)
async def update_user(self, user_id: str, **kwargs) -> Dict[str, Any]:
"""Actualiza información local del usuario (e identidad vía Hub si se implementa)"""
user_tenant = self.db.query(UserTenant).filter(
and_(UserTenant.keycloak_user_id == user_id, UserTenant.tenant_id == self.tenant_id)
).first()
if not user_tenant:
raise HTTPException(status_code=404, detail="User not found in this tenant")
raise HTTPException(status_code=404, detail="User not found")
try:
# Preparar datos de actualización para Keycloak
update_data = {}
if first_name is not None:
update_data["firstName"] = first_name
if last_name is not None:
update_data["lastName"] = last_name
if email is not None:
update_data["email"] = email
if enabled is not None:
update_data["enabled"] = enabled
if email_verified is not None:
update_data["emailVerified"] = email_verified
# Actualizar campos locales
for field in ["role", "avatar_url", "phone", "bio", "preferences"]:
if field in kwargs and kwargs[field] is not None:
setattr(user_tenant, field, kwargs[field])
# Actualizar en Keycloak si hay cambios
if update_data:
self.keycloak_admin.update_user(user_id, update_data)
self.db.commit()
self.db.refresh(user_tenant)
return _normalize_user({"id": user_id}, user_tenant.role, user_tenant)
# Actualizar campos en UserTenant
if role is not None:
user_tenant.role = role
if avatar_url is not None:
user_tenant.avatar_url = avatar_url
if phone is not None:
user_tenant.phone = phone
if bio is not None:
user_tenant.bio = bio
if preferences is not None:
user_tenant.preferences = preferences
async def delete_user(self, user_id: str, soft_delete: bool = True) -> None:
"""Elimina/Desactiva usuario"""
user_tenant = self.db.query(UserTenant).filter(
and_(UserTenant.keycloak_user_id == user_id, UserTenant.tenant_id == self.tenant_id)
).first()
if not user_tenant:
raise HTTPException(status_code=404, detail="User not found")
if soft_delete:
user_tenant.is_active = False
self.db.commit()
else:
# TODO: Call Hub to delete from Keycloak
self.db.delete(user_tenant)
self.db.commit()
self.db.refresh(user_tenant)
# Obtener información actualizada
user_info = self.keycloak_admin.get_user(user_id)
return _normalize_keycloak_user(user_info, user_tenant.role, user_tenant)
except KeycloakError as e:
logger.error(f"Error updating user in Keycloak: {str(e)}")
self.db.rollback()
raise HTTPException(
status_code=500, detail=f"Error updating user: {str(e)}"
)
def delete_user(self, user_id: str, soft_delete: bool = True) -> None:
"""
Elimina un usuario del tenant
Args:
user_id: ID del usuario en Keycloak
soft_delete: Si es True, solo desactiva. Si es False, elimina de Keycloak
"""
# Verificar que el usuario pertenece al tenant
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == user_id,
UserTenant.tenant_id == self.tenant_id,
)
)
.first()
)
if not user_tenant:
raise HTTPException(status_code=404, detail="User not found in this tenant")
async def change_password(self, user_id: str, password: str, temporary: bool = True) -> None:
"""Cambia contraseña vía Hub"""
try:
if soft_delete:
# Solo desactivar la relación
user_tenant.is_active = False
self.db.commit()
else:
# Eliminar permanentemente de Keycloak
self.keycloak_admin.delete_user(user_id)
# Eliminar relación
self.db.delete(user_tenant)
self.db.commit()
except KeycloakError as e:
logger.error(f"Error deleting user from Keycloak: {str(e)}")
self.db.rollback()
raise HTTPException(
status_code=500, detail=f"Error deleting user: {str(e)}"
)
def change_password(
self, user_id: str, password: str, temporary: bool = True
) -> None:
"""Cambia la contraseña de un usuario"""
# Verificar que el usuario pertenece al tenant
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == user_id,
UserTenant.tenant_id == self.tenant_id,
UserTenant.is_active == True,
async with httpx.AsyncClient(timeout=10.0) as client:
await client.post(
f"{settings.HUB_URL}api/v1/auth/change-password",
json={"user_id": user_id, "password": password, "temporary": temporary}
)
)
.first()
)
if not user_tenant:
raise HTTPException(status_code=404, detail="User not found in this tenant")
try:
self.keycloak_admin.set_user_password(
user_id, password, temporary=temporary
)
except KeycloakError as e:
logger.error(f"Error changing user password: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error changing password: {str(e)}"
)
except Exception as e:
logger.error(f"Error changing password: {e}")
raise HTTPException(status_code=500, detail="Error changing password")
def get_user_stats(self) -> Dict[str, Any]:
"""Obtiene estadísticas de usuarios del tenant"""
@@ -573,93 +373,19 @@ class UserService:
"usage_percentage": round(usage_percentage, 2),
}
def get_current_user_profile(self, keycloak_user_id: str) -> Dict[str, Any]:
"""
Obtiene el perfil completo del usuario actual
Combina datos de Keycloak con datos de UserTenant
"""
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == keycloak_user_id,
UserTenant.is_active == True,
)
)
.first()
)
async def get_current_user_profile(self, keycloak_user_id: str) -> Dict[str, Any]:
"""Obtiene el perfil completo del usuario actual"""
# Reutilizamos verify_token para obtener info del Hub
from core.security import verify_token
user_info = await verify_token(keycloak_user_id) # keycloak_user_id es el token en este contexto, o el ID
# Nota: en routes.py se pasa el ID. Si necesitamos info real, pedimos al Hub.
user_tenant = self.db.query(UserTenant).filter(
and_(UserTenant.keycloak_user_id == keycloak_user_id, UserTenant.is_active == True)
).first()
if not user_tenant:
raise HTTPException(status_code=404, detail="User profile not found")
return _normalize_user(user_info, user_tenant.role if user_tenant else None, user_tenant)
try:
user_info = self.keycloak_admin.get_user(keycloak_user_id)
return _normalize_keycloak_user(user_info, user_tenant.role, user_tenant)
except KeycloakError as e:
logger.error(f"Error getting user from Keycloak: {str(e)}")
raise HTTPException(status_code=404, detail="User not found")
def update_current_user_profile(
self,
keycloak_user_id: str,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
email: Optional[str] = None,
avatar_url: Optional[str] = None,
phone: Optional[str] = None,
bio: Optional[str] = None,
preferences: Optional[dict] = None,
) -> Dict[str, Any]:
"""
Actualiza el perfil del usuario actual
"""
user_tenant = (
self.db.query(UserTenant)
.filter(
and_(
UserTenant.keycloak_user_id == keycloak_user_id,
UserTenant.is_active == True,
)
)
.first()
)
if not user_tenant:
raise HTTPException(status_code=404, detail="User profile not found")
try:
# Actualizar Keycloak
update_data = {}
if first_name is not None:
update_data["firstName"] = first_name
if last_name is not None:
update_data["lastName"] = last_name
if email is not None:
update_data["email"] = email
if update_data:
self.keycloak_admin.update_user(keycloak_user_id, update_data)
# Actualizar campos de perfil en UserTenant
if avatar_url is not None:
user_tenant.avatar_url = avatar_url
if phone is not None:
user_tenant.phone = phone
if bio is not None:
user_tenant.bio = bio
if preferences is not None:
user_tenant.preferences = preferences
self.db.commit()
self.db.refresh(user_tenant)
# Retornar perfil actualizado
user_info = self.keycloak_admin.get_user(keycloak_user_id)
return _normalize_keycloak_user(user_info, user_tenant.role, user_tenant)
except KeycloakError as e:
logger.error(f"Error updating user profile: {str(e)}")
self.db.rollback()
raise HTTPException(
status_code=500, detail=f"Error updating profile: {str(e)}"
)
async def update_current_user_profile(self, keycloak_user_id: str, **kwargs) -> Dict[str, Any]:
"""Actualiza el perfil del usuario actual"""
return await self.update_user(keycloak_user_id, **kwargs)