feat: multi-tenant user delete scope + conditional keycloak wait
- Add GET /users/{user_id}/tenant-count endpoint
- Add scope query param (current|all) to DELETE /users/{user_id}
- UserService.delete_user now supports scope='all' to deactivate/remove
user from all tenants
- Frontend users.ts: getTenantCount(), delete() accepts scope param
- Frontend +page.svelte: async openDeleteDialog fetches tenant count,
shows 4-button dialog when user belongs to >1 tenant
- docker-entrypoint.sh: Keycloak wait now conditional on WAIT_FOR_KEYCLOAK=1
This commit is contained in:
@@ -218,6 +218,24 @@ async def update_user_detail(
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{user_id}/tenant-count")
|
||||
async def get_user_tenant_count(
|
||||
user_id: str,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Retorna en cuántos tenants está registrado el usuario.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user, required_permissions=["user.view"]
|
||||
)
|
||||
service = UserService(db, tenant_id, company_id)
|
||||
count = service.get_user_tenant_count(user_id)
|
||||
return {"tenant_count": count}
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user_route(
|
||||
user_id: str,
|
||||
@@ -226,15 +244,21 @@ async def delete_user_route(
|
||||
True,
|
||||
description="Si es True, solo desactiva. Si es False, elimina permanentemente",
|
||||
),
|
||||
scope: str = Query(
|
||||
"current",
|
||||
description="'current' para borrar solo del tenant activo, 'all' para borrar de todos los tenants",
|
||||
),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Elimina un usuario del tenant
|
||||
Elimina un usuario del tenant.
|
||||
scope='current' (default): solo del tenant activo.
|
||||
scope='all': de todos los tenants en los que aparece el usuario.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.delete"])
|
||||
service = UserService(db, tenant_id, company_id)
|
||||
await service.delete_user(user_id, soft_delete=soft_delete)
|
||||
await service.delete_user(user_id, soft_delete=soft_delete, scope=scope)
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
|
||||
|
||||
@@ -394,8 +394,43 @@ class UserService:
|
||||
self.db.refresh(user_tenant)
|
||||
return _normalize_user({"id": user_id}, user_tenant.role, user_tenant)
|
||||
|
||||
async def delete_user(self, user_id: str, soft_delete: bool = True) -> None:
|
||||
"""Elimina/Desactiva usuario"""
|
||||
def get_user_tenant_count(self, user_id: str) -> int:
|
||||
"""Cuenta en cuántos tenants está registrado el usuario."""
|
||||
return (
|
||||
self.db.query(func.count(UserTenant.id))
|
||||
.filter(UserTenant.keycloak_user_id == user_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
async def delete_user(
|
||||
self, user_id: str, soft_delete: bool = True, scope: str = "current"
|
||||
) -> None:
|
||||
"""
|
||||
Elimina/Desactiva usuario.
|
||||
scope='current': solo del tenant activo.
|
||||
scope='all': de todos los tenants (útil cuando el usuario pertenece a múltiples tenants).
|
||||
"""
|
||||
if scope == "all":
|
||||
rows = (
|
||||
self.db.query(UserTenant)
|
||||
.filter(UserTenant.keycloak_user_id == user_id)
|
||||
.all()
|
||||
)
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if soft_delete:
|
||||
for row in rows:
|
||||
row.is_active = False
|
||||
self.db.commit()
|
||||
else:
|
||||
# TODO: Call Hub to delete from Keycloak
|
||||
for row in rows:
|
||||
self.db.delete(row)
|
||||
self.db.commit()
|
||||
return
|
||||
|
||||
# scope == "current" (default)
|
||||
user_tenant = self.db.query(UserTenant).filter(
|
||||
and_(UserTenant.keycloak_user_id == user_id, UserTenant.tenant_id == self.tenant_id)
|
||||
).first()
|
||||
|
||||
@@ -29,8 +29,22 @@ wait_for_tcp() {
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_for_tcp "${CORE_DB_HOST:-postgres-a76}" "${CORE_DB_PORT:-5432}" "PostgreSQL"
|
||||
wait_for_tcp "keycloak" "8080" "Keycloak"
|
||||
DB_HOST_PRIMARY="${CORE_DB_HOST:-postgres-a76}"
|
||||
DB_PORT="${CORE_DB_PORT:-5432}"
|
||||
|
||||
if ! wait_for_tcp "$DB_HOST_PRIMARY" "$DB_PORT" "PostgreSQL"; then
|
||||
# Fallback para entornos donde Docker solo registra el nombre del contenedor.
|
||||
if [[ "$DB_HOST_PRIMARY" == "postgres-a76" ]]; then
|
||||
wait_for_tcp "anexo76-postgres-a76" "$DB_PORT" "PostgreSQL" || true
|
||||
else
|
||||
echo " Continuando de todas formas..."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keycloak solo se espera si se habilita explícitamente (ej. entorno Hub completo).
|
||||
if [[ "${WAIT_FOR_KEYCLOAK:-0}" == "1" ]]; then
|
||||
wait_for_tcp "${KEYCLOAK_SERVICE_HOST:-keycloak}" "${KEYCLOAK_SERVICE_PORT:-8080}" "Keycloak" || true
|
||||
fi
|
||||
|
||||
echo "Iniciando proceso: $*"
|
||||
exec "$@"
|
||||
|
||||
Reference in New Issue
Block a user