Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user