diff --git a/backend/api/v1/modules/core/users/routes.py b/backend/api/v1/modules/core/users/routes.py index adc0e862..3d4a4e6c 100644 --- a/backend/api/v1/modules/core/users/routes.py +++ b/backend/api/v1/modules/core/users/routes.py @@ -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"} diff --git a/backend/api/v1/modules/core/users/service.py b/backend/api/v1/modules/core/users/service.py index 3d39839c..c847ba3c 100644 --- a/backend/api/v1/modules/core/users/service.py +++ b/backend/api/v1/modules/core/users/service.py @@ -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() diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 69805452..bcd9d7c4 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -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 "$@" diff --git a/frontend/src/lib/api/dashboard/users.ts b/frontend/src/lib/api/dashboard/users.ts index f8992b0a..755db998 100644 --- a/frontend/src/lib/api/dashboard/users.ts +++ b/frontend/src/lib/api/dashboard/users.ts @@ -125,14 +125,33 @@ export const usersAPI = { return response.data!; }, + /** + * Retorna en cuántos tenants está registrado el usuario. + */ + async getTenantCount(userId: string, companyId: number): Promise { + const response = await api.get<{ tenant_count: number }>( + `/v1/core/users/${userId}/tenant-count?company_id=${companyId}` + ); + if (response.error) { + throw new Error(response.error); + } + return response.data!.tenant_count; + }, + /** * Elimina un usuario */ - async delete(userId: string, companyId: number, softDelete: boolean = true): Promise { + async delete( + userId: string, + companyId: number, + softDelete: boolean = true, + scope: 'current' | 'all' = 'current' + ): Promise { const queryParams = new URLSearchParams(); queryParams.set('company_id', companyId.toString()); queryParams.set('soft_delete', softDelete.toString()); - + queryParams.set('scope', scope); + const response = await api.delete(`/v1/core/users/${userId}?${queryParams}`); if (response.error) { throw new Error(response.error); diff --git a/frontend/src/routes/dashboard/users/+page.svelte b/frontend/src/routes/dashboard/users/+page.svelte index 6efdc635..5d92a654 100644 --- a/frontend/src/routes/dashboard/users/+page.svelte +++ b/frontend/src/routes/dashboard/users/+page.svelte @@ -257,6 +257,10 @@ let showPasswordDialog = $state(false); let showRolesDialog = $state(false); + // Estado de borrado multi-tenant + let deleteUserTenantCount = $state(0); + let deleteLoadingCount = $state(false); + // Estados para gestión de roles let availableRoles = $state([]); let userRoles = $state([]); @@ -631,13 +635,28 @@ } // Abrir diálogo de eliminación - function openDeleteDialog(user: User) { + async function openDeleteDialog(user: User) { selectedUser = user; + deleteUserTenantCount = 0; + deleteLoadingCount = true; showDeleteDialog = true; + try { + const companyId = companyStore.activeCompany?.id; + if (companyId) { + deleteUserTenantCount = await usersAPI.getTenantCount(user.id, companyId); + } + } catch { + deleteUserTenantCount = 1; + } finally { + deleteLoadingCount = false; + } } // Eliminar usuario - async function handleDelete(softDelete: boolean = true) { + async function handleDelete( + softDelete: boolean = true, + scope: 'current' | 'all' = 'current' + ) { if (!selectedUser) return; const companyId = companyStore.activeCompany?.id; @@ -647,9 +666,12 @@ } try { - await usersAPI.delete(selectedUser.id, companyId, softDelete); + await usersAPI.delete(selectedUser.id, companyId, softDelete, scope); + const scopeMsg = scope === 'all' ? ' de todos los tenants' : ''; toast.success( - softDelete ? 'Usuario desactivado exitosamente' : 'Usuario eliminado permanentemente' + softDelete + ? `Usuario desactivado exitosamente${scopeMsg}` + : `Usuario eliminado permanentemente${scopeMsg}` ); showDeleteDialog = false; selectedUser = null; @@ -2530,24 +2552,62 @@ ¿Eliminar usuario? - ¿Estás seguro de que deseas eliminar a {selectedUser?.username}? + ¿Estás seguro de que deseas eliminar a + {selectedUser?.username}?

- Puedes desactivar el usuario (recomendado) o eliminarlo permanentemente. + {#if deleteLoadingCount} + Verificando membresías... + {:else if deleteUserTenantCount > 1} + + ⚠️ Este usuario pertenece a + {deleteUserTenantCount} tenants. Puedes eliminarlo solo del + tenant actual o de todos. + + {/if}
- + Cancelar - - handleDelete(false)} - class="text-destructive-foreground bg-destructive hover:bg-destructive/90" - > - - Eliminar Permanente - + + {#if !deleteLoadingCount && deleteUserTenantCount > 1} + + + handleDelete(false, 'current')} + class="text-destructive-foreground bg-destructive hover:bg-destructive/90" + > + + Eliminar (este tenant) + + + + handleDelete(false, 'all')} + class="text-destructive-foreground bg-destructive hover:bg-destructive/90" + > + + Eliminar (todos) + + {:else} + + + handleDelete(false, 'current')} + class="text-destructive-foreground bg-destructive hover:bg-destructive/90" + > + + Eliminar Permanente + + {/if}