feat: add forced WAP server deletion support to clean up associated organizations and runtime bots
This commit is contained in:
Binary file not shown.
@@ -143,11 +143,19 @@ export type SysAdminWapServerAuditParams = SysAdminWapServerByIdParams;
|
||||
|
||||
export type SysAdminWapServerRecalculateCountParams = SysAdminWapServerByIdParams;
|
||||
|
||||
export type SysAdminDeleteWapServerParams = SysAdminWapServerByIdParams;
|
||||
export type SysAdminDeleteWapServerParams = SysAdminWapServerByIdParams & {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type SysAdminDeleteWapServerResult = {
|
||||
serverId: string;
|
||||
deleted: true;
|
||||
force?: boolean;
|
||||
detachedOrganizations?: number;
|
||||
deletedRuntimeBots?: number;
|
||||
skippedRuntimeBots?: number;
|
||||
auditFailed?: boolean;
|
||||
auditMessage?: string;
|
||||
};
|
||||
|
||||
export type SysAdminWapServerOrganizationActionParams = SysAdminWapServerByIdParams & {
|
||||
|
||||
@@ -1157,33 +1157,85 @@ class WapServerManager implements IWapServerManager {
|
||||
throw new Error("WAP server not found");
|
||||
}
|
||||
|
||||
if (server.active) {
|
||||
const force = data.force === true;
|
||||
|
||||
if (!force && server.active) {
|
||||
throw new Error("WAP server must be inactive before deletion");
|
||||
}
|
||||
|
||||
const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId });
|
||||
|
||||
if (assignedCompanies.length > 0) {
|
||||
if (!force && assignedCompanies.length > 0) {
|
||||
throw new Error(`WAP server cannot be deleted because it has ${assignedCompanies.length} associated bot(s) in DB`);
|
||||
}
|
||||
|
||||
let detectedBots: SysAdminWapContainerDto[];
|
||||
let detectedBots: SysAdminWapContainerDto[] = [];
|
||||
let auditFailed = false;
|
||||
let auditMessage: string | undefined;
|
||||
|
||||
try {
|
||||
detectedBots = await this.getSysAdminRuntimeContainers(server);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message ? error.message : "unknown error";
|
||||
|
||||
throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`);
|
||||
if (!force) {
|
||||
throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`);
|
||||
}
|
||||
|
||||
auditFailed = true;
|
||||
auditMessage = message;
|
||||
}
|
||||
|
||||
if (detectedBots.length > 0) {
|
||||
if (!force && detectedBots.length > 0) {
|
||||
throw new Error(`WAP server cannot be deleted because bot-admin detected ${detectedBots.length} runtime bot(s)`);
|
||||
}
|
||||
|
||||
let deletedRuntimeBots = 0;
|
||||
let skippedRuntimeBots = 0;
|
||||
|
||||
if (force) {
|
||||
const baseUrl = this.getBotAdminApiUrl(server.ipv4);
|
||||
|
||||
for (const bot of detectedBots) {
|
||||
if (!bot.organizationId) {
|
||||
skippedRuntimeBots += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.deleteSysAdminRuntimeBot(baseUrl, encodeURIComponent(bot.organizationId), true);
|
||||
|
||||
if (result.botDeleted) {
|
||||
deletedRuntimeBots += 1;
|
||||
}
|
||||
} catch (_error) {
|
||||
skippedRuntimeBots += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const company of assignedCompanies) {
|
||||
const companyDocument = company as typeof company & { save: () => Promise<unknown> };
|
||||
|
||||
companyDocument.wapServerId = undefined;
|
||||
await companyDocument.save();
|
||||
}
|
||||
}
|
||||
|
||||
await this.servers.delete(data.serverId);
|
||||
|
||||
return { serverId: data.serverId, deleted: true };
|
||||
return {
|
||||
serverId: data.serverId,
|
||||
deleted: true,
|
||||
...(force
|
||||
? {
|
||||
force: true,
|
||||
detachedOrganizations: assignedCompanies.length,
|
||||
deletedRuntimeBots,
|
||||
skippedRuntimeBots,
|
||||
...(auditFailed ? { auditFailed: true, auditMessage } : {}),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
public async sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise<void> {
|
||||
|
||||
@@ -254,6 +254,17 @@ export type WapServerRecalculateCountResult = {
|
||||
export type WapServerDeleteResult = {
|
||||
serverId: string;
|
||||
deleted: true;
|
||||
force?: boolean;
|
||||
detachedOrganizations?: number;
|
||||
deletedRuntimeBots?: number;
|
||||
skippedRuntimeBots?: number;
|
||||
auditFailed?: boolean;
|
||||
auditMessage?: string;
|
||||
};
|
||||
|
||||
export type WapServerDeletePayload = {
|
||||
serverId: string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type WapServerOrganizationActionPayload = {
|
||||
@@ -282,8 +293,8 @@ export const recalculateWapServerCount = async (serverId: string): Promise<WapSe
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const deleteWapServer = async (serverId: string): Promise<WapServerDeleteResult> => {
|
||||
const res = await sysadminApi.post('/wap/delete', { serverId });
|
||||
export const deleteWapServer = async (data: WapServerDeletePayload): Promise<WapServerDeleteResult> => {
|
||||
const res = await sysadminApi.post('/wap/delete', data);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Search, RotateCcw, CheckCircle, XCircle, Server, ServerOff, ClipboardList, Calculator, X, MoreVertical, Play, Pause, Power, Trash2, Unlink, QrCode, RotateCw, Edit3, Plus } from 'lucide-react';
|
||||
import { auditWapServer, createWapServer, deleteWapBot, deleteWapServer, detachWapOrganizationServer, fetchWapServers, getWapBotQr, recalculateWapServerCount, restartWapBot, startWapBot, stopWapBot, updateWapServer, type WapContainerDto, type WapServerAuditResult, type WapServerQrResult, type WapServerRecalculateCountResult } from '../api/sysadmin';
|
||||
import { auditWapServer, createWapServer, deleteWapBot, deleteWapServer, detachWapOrganizationServer, fetchWapServers, getWapBotQr, recalculateWapServerCount, restartWapBot, startWapBot, stopWapBot, updateWapServer, type WapContainerDto, type WapServerAuditResult, type WapServerDeleteResult, type WapServerQrResult, type WapServerRecalculateCountResult } from '../api/sysadmin';
|
||||
|
||||
const getErrorMessage = (error: unknown, fallback: string): string => {
|
||||
const responseData = (error as any)?.response?.data;
|
||||
@@ -30,6 +30,8 @@ export const WapServersPage = () => {
|
||||
const [openActionsServerId, setOpenActionsServerId] = useState<string | null>(null);
|
||||
const [openAuditActionKey, setOpenAuditActionKey] = useState<string | null>(null);
|
||||
const [editingServer, setEditingServer] = useState<any | null>(null);
|
||||
const [deleteServerTarget, setDeleteServerTarget] = useState<any | null>(null);
|
||||
const [forceDeleteServer, setForceDeleteServer] = useState(false);
|
||||
const [serverFormMode, setServerFormMode] = useState<'create' | 'edit'>('edit');
|
||||
const [editForm, setEditForm] = useState({ name: '', ipv4: '', port: '', maxBots: '', active: true });
|
||||
|
||||
@@ -199,18 +201,28 @@ export const WapServersPage = () => {
|
||||
};
|
||||
|
||||
const handleDeleteServer = async (server: any) => {
|
||||
setForceDeleteServer(false);
|
||||
setDeleteServerTarget(server);
|
||||
};
|
||||
|
||||
const confirmDeleteServer = async () => {
|
||||
if (!deleteServerTarget) return;
|
||||
|
||||
const server = deleteServerTarget;
|
||||
const serverId = getServerId(server);
|
||||
const serverLabel = server.name || serverId;
|
||||
|
||||
if (!window.confirm(`Confirmá eliminar el servidor WAP ${serverLabel}. Debe estar inactivo y sin bots asociados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionServerId(serverId);
|
||||
try {
|
||||
await deleteWapServer(serverId);
|
||||
setActionMessage(`Servidor WAP eliminado: ${serverLabel}.`);
|
||||
const result: WapServerDeleteResult = await deleteWapServer({ serverId, force: forceDeleteServer || undefined });
|
||||
const forceSummary = result.force
|
||||
? ` Modo forzado: organizaciones desasociadas ${result.detachedOrganizations ?? 0}, bots runtime eliminados ${result.deletedRuntimeBots ?? 0}, bots omitidos ${result.skippedRuntimeBots ?? 0}${result.auditFailed ? `. Auditoría no disponible: ${result.auditMessage || 'sin detalle'}` : ''}.`
|
||||
: '';
|
||||
|
||||
setActionMessage(`Servidor WAP eliminado: ${serverLabel}.${forceSummary}`);
|
||||
setSelectedAuditServerId(null);
|
||||
setDeleteServerTarget(null);
|
||||
setForceDeleteServer(false);
|
||||
await loadServers();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -630,6 +642,38 @@ export const WapServersPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deleteServerTarget && (
|
||||
<div className="modal-overlay" onClick={() => setDeleteServerTarget(null)}>
|
||||
<div className="delete-server-modal glass-panel" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="audit-modal-header">
|
||||
<div>
|
||||
<h2>Eliminar servidor WAP</h2>
|
||||
<p>{deleteServerTarget.name || getServerId(deleteServerTarget)}</p>
|
||||
</div>
|
||||
<button type="button" className="btn-secondary" onClick={() => setDeleteServerTarget(null)} aria-label="Cerrar confirmación">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="delete-warning">
|
||||
<strong>Esta acción elimina el servidor WAP.</strong>
|
||||
<p>Sin forzar, el servidor debe estar inactivo y no tener bots asociados.</p>
|
||||
<p>Si activás borrado forzado, se omiten esas validaciones: las organizaciones asignadas se desasocian y los bots/contenedores runtime detectados pueden eliminarse u omitirse si no se pueden identificar con seguridad.</p>
|
||||
</div>
|
||||
|
||||
<label className="force-delete-option">
|
||||
<input type="checkbox" checked={forceDeleteServer} onChange={(event) => setForceDeleteServer(event.target.checked)} />
|
||||
<span>Forzar eliminación y desasociar organizaciones/bots aunque el servidor esté activo.</span>
|
||||
</label>
|
||||
|
||||
<div className="edit-server-actions">
|
||||
<button type="button" className="btn-secondary" onClick={() => setDeleteServerTarget(null)}>Cancelar</button>
|
||||
<button type="button" className="btn-danger" disabled={actionServerId === getServerId(deleteServerTarget)} onClick={confirmDeleteServer}>Eliminar servidor</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recalculateResult && (
|
||||
<div className="modal-overlay" onClick={() => setRecalculateResult(null)}>
|
||||
<div className="recalculate-modal glass-panel" onClick={(event) => event.stopPropagation()}>
|
||||
@@ -798,6 +842,52 @@ export const WapServersPage = () => {
|
||||
width: min(560px, 96vw);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.delete-server-modal {
|
||||
width: min(560px, 96vw);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.delete-warning {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(239, 68, 68, 0.32);
|
||||
border-radius: 14px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--text-main);
|
||||
}
|
||||
.delete-warning p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.force-delete-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.9rem;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
color: #fecaca;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
cursor: pointer;
|
||||
}
|
||||
.force-delete-option input {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
.btn-danger {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
color: #ffffff;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-danger:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.edit-server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
import { Request, Response } from "express";
|
||||
import { WapService } from "../services/wap.service";
|
||||
import { CreateWapServerParams, PaginateWapServerParams, UpdateWapServerParams, WapServerByIdParams, WapServerOrganizationActionParams } from "src/models/WapServers.Model";
|
||||
import { CreateWapServerParams, PaginateWapServerParams, UpdateWapServerParams, WapServerByIdParams, WapServerDeleteParams, WapServerOrganizationActionParams } from "src/models/WapServers.Model";
|
||||
|
||||
const wapService = new WapService();
|
||||
|
||||
@@ -83,7 +83,7 @@ export class WapController {
|
||||
|
||||
public deleteServer = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const data: WapServerByIdParams = req.body as WapServerByIdParams;
|
||||
const data: WapServerDeleteParams = req.body as WapServerDeleteParams;
|
||||
const result = await wapService.deleteWapServer(data);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
|
||||
@@ -62,9 +62,19 @@ export type WapServerByIdParams = {
|
||||
serverId: string;
|
||||
};
|
||||
|
||||
export type WapServerDeleteParams = WapServerByIdParams & {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type WapServerDeleteResult = {
|
||||
serverId: string;
|
||||
deleted: true;
|
||||
force?: boolean;
|
||||
detachedOrganizations?: number;
|
||||
deletedRuntimeBots?: number;
|
||||
skippedRuntimeBots?: number;
|
||||
auditFailed?: boolean;
|
||||
auditMessage?: string;
|
||||
};
|
||||
|
||||
export type WapServerOrganizationActionParams = WapServerByIdParams & {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PaginateWapServerResults,
|
||||
WapServerAuditResult,
|
||||
WapServerByIdParams,
|
||||
WapServerDeleteParams,
|
||||
WapServerDeleteResult,
|
||||
WapServerDetachResult,
|
||||
WapServerOrganizationActionParams,
|
||||
@@ -82,7 +83,7 @@ export class WapService {
|
||||
return response.data as WapServerRecalculateCountResult;
|
||||
}
|
||||
|
||||
public async deleteWapServer(data: WapServerByIdParams): Promise<WapServerDeleteResult> {
|
||||
public async deleteWapServer(data: WapServerDeleteParams): Promise<WapServerDeleteResult> {
|
||||
const nonce = await getSysAdminNonce();
|
||||
const postData = { ...data, ...{ payload: getPayload(nonce) } };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user