diff --git a/cli/commands/deleteClient.ts b/cli/commands/deleteClient.ts new file mode 100644 index 00000000..28fef50d --- /dev/null +++ b/cli/commands/deleteClient.ts @@ -0,0 +1,123 @@ +import { CommandModule } from "yargs"; +import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db"; +import { eq, and, inArray } from "drizzle-orm"; + +type DeleteClientArgs = { + orgId: string; + niceId: string; +}; + +export const deleteClient: CommandModule<{}, DeleteClientArgs> = { + command: "delete-client", + describe: + "Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.", + builder: (yargs) => { + return yargs + .option("orgId", { + type: "string", + demandOption: true, + describe: "The organization ID" + }) + .option("niceId", { + type: "string", + demandOption: true, + describe: "The client niceId (identifier)" + }); + }, + handler: async (argv: { orgId: string; niceId: string }) => { + try { + const { orgId, niceId } = argv; + + console.log( + `Deleting client with orgId: ${orgId}, niceId: ${niceId}...` + ); + + // Find the client + const [client] = await db + .select() + .from(clients) + .where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId))) + .limit(1); + + if (!client) { + console.error( + `Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.` + ); + process.exit(1); + } + + const clientId = client.clientId; + console.log(`Found client with clientId: ${clientId}`); + + // Find all OLMs associated with this client + const associatedOlms = await db + .select() + .from(olms) + .where(eq(olms.clientId, clientId)); + + console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`); + + // Delete in a transaction to ensure atomicity + await db.transaction(async (trx) => { + // Delete currentFingerprint entries for the associated OLMs + // Note: We delete these explicitly before deleting OLMs to ensure + // we have control, even though cascade would handle it + let fingerprintCount = 0; + if (associatedOlms.length > 0) { + const olmIds = associatedOlms.map((olm) => olm.olmId); + const deletedFingerprints = await trx + .delete(currentFingerprint) + .where(inArray(currentFingerprint.olmId, olmIds)) + .returning(); + fingerprintCount = deletedFingerprints.length; + } + console.log(`Deleted ${fingerprintCount} current fingerprint(s)`); + + // Delete OLMs + // Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly + const deletedOlms = await trx + .delete(olms) + .where(eq(olms.clientId, clientId)) + .returning(); + console.log(`Deleted ${deletedOlms.length} OLM(s)`); + + // Delete approvals + // Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity + const deletedApprovals = await trx + .delete(approvals) + .where(eq(approvals.clientId, clientId)) + .returning(); + console.log(`Deleted ${deletedApprovals.length} approval(s)`); + + // Delete userClients + // Note: userClients have onDelete: "cascade" but we delete explicitly for clarity + const deletedUserClients = await trx + .delete(userClients) + .where(eq(userClients.clientId, clientId)) + .returning(); + console.log(`Deleted ${deletedUserClients.length} userClient association(s)`); + + // Finally, delete the client itself + const deletedClients = await trx + .delete(clients) + .where(eq(clients.clientId, clientId)) + .returning(); + console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`); + }); + + console.log("\nClient deletion completed successfully!"); + console.log("\nSummary:"); + console.log(` - Client: ${niceId} (clientId: ${clientId})`); + console.log(` - Olm(s): ${associatedOlms.length}`); + console.log(` - Current fingerprints: deleted`); + console.log(` - Approvals: deleted`); + console.log(` - UserClients: deleted`); + console.log(` - Snapshots: preserved (not deleted)`); + + process.exit(0); + } catch (error) { + console.error("Error deleting client:", error); + process.exit(1); + } + } +}; diff --git a/cli/index.ts b/cli/index.ts index 328520aa..d517064c 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -7,6 +7,7 @@ import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys"; import { clearExitNodes } from "./commands/clearExitNodes"; import { rotateServerSecret } from "./commands/rotateServerSecret"; import { clearLicenseKeys } from "./commands/clearLicenseKeys"; +import { deleteClient } from "./commands/deleteClient"; yargs(hideBin(process.argv)) .scriptName("pangctl") @@ -15,5 +16,6 @@ yargs(hideBin(process.argv)) .command(clearExitNodes) .command(rotateServerSecret) .command(clearLicenseKeys) + .command(deleteClient) .demandCommand() .help().argv;