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 SysAdminWapServerRecalculateCountParams = SysAdminWapServerByIdParams;
|
||||||
|
|
||||||
export type SysAdminDeleteWapServerParams = SysAdminWapServerByIdParams;
|
export type SysAdminDeleteWapServerParams = SysAdminWapServerByIdParams & {
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type SysAdminDeleteWapServerResult = {
|
export type SysAdminDeleteWapServerResult = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
deleted: true;
|
deleted: true;
|
||||||
|
force?: boolean;
|
||||||
|
detachedOrganizations?: number;
|
||||||
|
deletedRuntimeBots?: number;
|
||||||
|
skippedRuntimeBots?: number;
|
||||||
|
auditFailed?: boolean;
|
||||||
|
auditMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SysAdminWapServerOrganizationActionParams = SysAdminWapServerByIdParams & {
|
export type SysAdminWapServerOrganizationActionParams = SysAdminWapServerByIdParams & {
|
||||||
|
|||||||
@@ -1157,33 +1157,85 @@ class WapServerManager implements IWapServerManager {
|
|||||||
throw new Error("WAP server not found");
|
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");
|
throw new Error("WAP server must be inactive before deletion");
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignedCompanies = await CompaniesManager.companies.find({ wapServerId: data.serverId });
|
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`);
|
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 {
|
try {
|
||||||
detectedBots = await this.getSysAdminRuntimeContainers(server);
|
detectedBots = await this.getSysAdminRuntimeContainers(server);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error && error.message ? error.message : "unknown error";
|
const message = error instanceof Error && error.message ? error.message : "unknown error";
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`);
|
throw new Error(`WAP server cannot be deleted because bot-admin could not be audited. Verify runtime bots manually before deleting. ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detectedBots.length > 0) {
|
auditFailed = true;
|
||||||
|
auditMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && detectedBots.length > 0) {
|
||||||
throw new Error(`WAP server cannot be deleted because bot-admin detected ${detectedBots.length} runtime bot(s)`);
|
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);
|
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> {
|
public async sysAdminStartBot(data: SysAdminWapServerOrganizationActionParams): Promise<void> {
|
||||||
|
|||||||
@@ -254,6 +254,17 @@ export type WapServerRecalculateCountResult = {
|
|||||||
export type WapServerDeleteResult = {
|
export type WapServerDeleteResult = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
deleted: true;
|
deleted: true;
|
||||||
|
force?: boolean;
|
||||||
|
detachedOrganizations?: number;
|
||||||
|
deletedRuntimeBots?: number;
|
||||||
|
skippedRuntimeBots?: number;
|
||||||
|
auditFailed?: boolean;
|
||||||
|
auditMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WapServerDeletePayload = {
|
||||||
|
serverId: string;
|
||||||
|
force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WapServerOrganizationActionPayload = {
|
export type WapServerOrganizationActionPayload = {
|
||||||
@@ -282,8 +293,8 @@ export const recalculateWapServerCount = async (serverId: string): Promise<WapSe
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteWapServer = async (serverId: string): Promise<WapServerDeleteResult> => {
|
export const deleteWapServer = async (data: WapServerDeletePayload): Promise<WapServerDeleteResult> => {
|
||||||
const res = await sysadminApi.post('/wap/delete', { serverId });
|
const res = await sysadminApi.post('/wap/delete', data);
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
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 { 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 getErrorMessage = (error: unknown, fallback: string): string => {
|
||||||
const responseData = (error as any)?.response?.data;
|
const responseData = (error as any)?.response?.data;
|
||||||
@@ -30,6 +30,8 @@ export const WapServersPage = () => {
|
|||||||
const [openActionsServerId, setOpenActionsServerId] = useState<string | null>(null);
|
const [openActionsServerId, setOpenActionsServerId] = useState<string | null>(null);
|
||||||
const [openAuditActionKey, setOpenAuditActionKey] = useState<string | null>(null);
|
const [openAuditActionKey, setOpenAuditActionKey] = useState<string | null>(null);
|
||||||
const [editingServer, setEditingServer] = useState<any | 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 [serverFormMode, setServerFormMode] = useState<'create' | 'edit'>('edit');
|
||||||
const [editForm, setEditForm] = useState({ name: '', ipv4: '', port: '', maxBots: '', active: true });
|
const [editForm, setEditForm] = useState({ name: '', ipv4: '', port: '', maxBots: '', active: true });
|
||||||
|
|
||||||
@@ -199,18 +201,28 @@ export const WapServersPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteServer = async (server: any) => {
|
const handleDeleteServer = async (server: any) => {
|
||||||
|
setForceDeleteServer(false);
|
||||||
|
setDeleteServerTarget(server);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeleteServer = async () => {
|
||||||
|
if (!deleteServerTarget) return;
|
||||||
|
|
||||||
|
const server = deleteServerTarget;
|
||||||
const serverId = getServerId(server);
|
const serverId = getServerId(server);
|
||||||
const serverLabel = server.name || serverId;
|
const serverLabel = server.name || serverId;
|
||||||
|
|
||||||
if (!window.confirm(`Confirmá eliminar el servidor WAP ${serverLabel}. Debe estar inactivo y sin bots asociados.`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionServerId(serverId);
|
setActionServerId(serverId);
|
||||||
try {
|
try {
|
||||||
await deleteWapServer(serverId);
|
const result: WapServerDeleteResult = await deleteWapServer({ serverId, force: forceDeleteServer || undefined });
|
||||||
setActionMessage(`Servidor WAP eliminado: ${serverLabel}.`);
|
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);
|
setSelectedAuditServerId(null);
|
||||||
|
setDeleteServerTarget(null);
|
||||||
|
setForceDeleteServer(false);
|
||||||
await loadServers();
|
await loadServers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -630,6 +642,38 @@ export const WapServersPage = () => {
|
|||||||
</div>
|
</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 && (
|
{recalculateResult && (
|
||||||
<div className="modal-overlay" onClick={() => setRecalculateResult(null)}>
|
<div className="modal-overlay" onClick={() => setRecalculateResult(null)}>
|
||||||
<div className="recalculate-modal glass-panel" onClick={(event) => event.stopPropagation()}>
|
<div className="recalculate-modal glass-panel" onClick={(event) => event.stopPropagation()}>
|
||||||
@@ -798,6 +842,52 @@ export const WapServersPage = () => {
|
|||||||
width: min(560px, 96vw);
|
width: min(560px, 96vw);
|
||||||
padding: 1.25rem;
|
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 {
|
.edit-server-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { WapService } from "../services/wap.service";
|
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();
|
const wapService = new WapService();
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export class WapController {
|
|||||||
|
|
||||||
public deleteServer = async (req: Request, res: Response): Promise<void> => {
|
public deleteServer = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const data: WapServerByIdParams = req.body as WapServerByIdParams;
|
const data: WapServerDeleteParams = req.body as WapServerDeleteParams;
|
||||||
const result = await wapService.deleteWapServer(data);
|
const result = await wapService.deleteWapServer(data);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -62,9 +62,19 @@ export type WapServerByIdParams = {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WapServerDeleteParams = WapServerByIdParams & {
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type WapServerDeleteResult = {
|
export type WapServerDeleteResult = {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
deleted: true;
|
deleted: true;
|
||||||
|
force?: boolean;
|
||||||
|
detachedOrganizations?: number;
|
||||||
|
deletedRuntimeBots?: number;
|
||||||
|
skippedRuntimeBots?: number;
|
||||||
|
auditFailed?: boolean;
|
||||||
|
auditMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WapServerOrganizationActionParams = WapServerByIdParams & {
|
export type WapServerOrganizationActionParams = WapServerByIdParams & {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
PaginateWapServerResults,
|
PaginateWapServerResults,
|
||||||
WapServerAuditResult,
|
WapServerAuditResult,
|
||||||
WapServerByIdParams,
|
WapServerByIdParams,
|
||||||
|
WapServerDeleteParams,
|
||||||
WapServerDeleteResult,
|
WapServerDeleteResult,
|
||||||
WapServerDetachResult,
|
WapServerDetachResult,
|
||||||
WapServerOrganizationActionParams,
|
WapServerOrganizationActionParams,
|
||||||
@@ -82,7 +83,7 @@ export class WapService {
|
|||||||
return response.data as WapServerRecalculateCountResult;
|
return response.data as WapServerRecalculateCountResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteWapServer(data: WapServerByIdParams): Promise<WapServerDeleteResult> {
|
public async deleteWapServer(data: WapServerDeleteParams): Promise<WapServerDeleteResult> {
|
||||||
const nonce = await getSysAdminNonce();
|
const nonce = await getSysAdminNonce();
|
||||||
const postData = { ...data, ...{ payload: getPayload(nonce) } };
|
const postData = { ...data, ...{ payload: getPayload(nonce) } };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user