Skip to content

Commit 6d82fb3

Browse files
committed
fix(backend): properly delete MCP installations and notify satellites during team deletion
- Delete each MCP installation from database before team deletion - Create satellite commands to kill running MCP server processes - Preserve pending satellite commands by setting target_team_id to NULL - Delete only completed/failed satellite commands to avoid foreign key constraints - Add email notifications for team creation and deletion events - Track installation deletion count and satellite commands created in logs
1 parent f394430 commit 6d82fb3

File tree

6 files changed

+232
-6
lines changed

6 files changed

+232
-6
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//- @description Email notification when a new team is created
2+
//- @variables userName, teamName, teamDescription
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 Team Created Successfully
7+
8+
p Hi #{userName},
9+
10+
p Your new team has been created successfully!
11+
12+
.team-info
13+
p
14+
strong Team Name:
15+
| #{teamName}
16+
17+
if teamDescription
18+
p
19+
strong Description:
20+
| #{teamDescription}
21+
22+
h2 What's next?
23+
24+
p Now that your team is set up, here are some things you can do:
25+
26+
ul
27+
li
28+
strong Add MCP servers 
29+
| - Browse our catalog and add tools to your team. MCP servers give you access to powerful integrations like GitHub, databases, and more.
30+
li
31+
strong Add team members 
32+
| - Invite your colleagues to collaborate. Share secure access to MCP tools without sharing credentials.
33+
34+
p
35+
| Visit your dashboard to get started with your new team.
36+
37+
p.text-muted
38+
| Best regards,
39+
br
40+
| The DeployStack Team
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//- @description Email notification when a team is deleted
2+
//- @variables userName, teamName
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 Team Deleted
7+
8+
p Hi #{userName},
9+
10+
p Your team #[strong #{teamName}] has been removed from DeployStack.
11+
12+
h2 What was removed?
13+
14+
p The following items were removed as part of the team deletion:
15+
16+
ul
17+
li
18+
strong All team members 
19+
| - All users have been removed from the team
20+
li
21+
strong All MCP servers 
22+
| - All MCP server installations have been uninstalled
23+
24+
p
25+
| If you need to use MCP servers again, you can create a new team or use your default team.
26+
27+
p.text-muted
28+
| Best regards,
29+
br
30+
| The DeployStack Team

services/backend/src/email/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ export interface McpInstallationDeletedEmailVariables {
152152
teamName: string;
153153
}
154154

155+
export interface TeamCreatedEmailVariables {
156+
userName: string;
157+
teamName: string;
158+
teamDescription?: string;
159+
}
160+
161+
export interface TeamDeletedEmailVariables {
162+
userName: string;
163+
teamName: string;
164+
}
165+
155166
// Template registry for type safety
156167
export interface TemplateVariableMap {
157168
welcome: WelcomeEmailVariables;
@@ -162,6 +173,8 @@ export interface TemplateVariableMap {
162173
test: TestEmailVariables;
163174
'mcp-installation-created': McpInstallationCreatedEmailVariables;
164175
'mcp-installation-deleted': McpInstallationDeletedEmailVariables;
176+
'team-created': TeamCreatedEmailVariables;
177+
'team-deleted': TeamDeletedEmailVariables;
165178
}
166179

167180
export type TemplateNames = keyof TemplateVariableMap;

services/backend/src/routes/teams/createTeam.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import type { FastifyInstance } from 'fastify';
23
import { TeamService } from '../../services/teamService';
34
import { requirePermission } from '../../middleware/roleMiddleware';
@@ -93,14 +94,39 @@ export default async function createTeamRoute(server: FastifyInstance) {
9394
owner_id: request.user.id,
9495
});
9596

97+
// Queue team creation email
98+
try {
99+
const jobQueueService = (server as any).jobQueueService;
100+
if (jobQueueService) {
101+
await jobQueueService.createJob('send_email', {
102+
to: (request.user as any).email,
103+
subject: 'Team Created Successfully',
104+
template: 'team-created',
105+
variables: {
106+
userName: (request.user as any).username || (request.user as any).email,
107+
teamName: team.name,
108+
teamDescription: team.description || undefined
109+
}
110+
});
111+
server.log.info({
112+
operation: 'team_created',
113+
teamId: team.id,
114+
userEmail: (request.user as any).email
115+
}, 'Team creation email queued');
116+
}
117+
} catch (emailError) {
118+
server.log.error(emailError, 'Failed to queue team creation email - team creation succeeded but email not sent');
119+
// Don't fail team creation if email queueing fails
120+
}
121+
96122
// Emit TEAM_CREATED event
97123
try {
98124
const eventContext: EventContext = {
99125
db: server.db,
100126
logger: server.log,
101127
user: {
102128
id: request.user.id,
103-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
129+
104130
email: (request.user as any).email,
105131
roleId: 'unknown' // We'd need to fetch this from DB if needed
106132
},
@@ -122,7 +148,7 @@ export default async function createTeamRoute(server: FastifyInstance) {
122148
},
123149
createdBy: {
124150
id: request.user.id,
125-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
151+
126152
email: (request.user as any).email
127153
},
128154
metadata: {

services/backend/src/routes/teams/deleteTeam.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import type { FastifyInstance, FastifyRequest } from 'fastify';
23
import { TeamService } from '../../services/teamService';
34
import { requirePermission } from '../../middleware/roleMiddleware';
@@ -87,7 +88,31 @@ export default async function deleteTeamRoute(server: FastifyInstance) {
8788
}
8889

8990
// Delete the team
90-
await TeamService.deleteTeam(teamId);
91+
await TeamService.deleteTeam(teamId, request.user!.id, server.log);
92+
93+
// Queue team deletion email
94+
try {
95+
const jobQueueService = (server as any).jobQueueService;
96+
if (jobQueueService) {
97+
await jobQueueService.createJob('send_email', {
98+
to: (request.user as any).email,
99+
subject: 'Team Deleted Successfully',
100+
template: 'team-deleted',
101+
variables: {
102+
userName: (request.user as any).username || (request.user as any).email,
103+
teamName: existingTeam.name
104+
}
105+
});
106+
server.log.info({
107+
operation: 'team_deleted',
108+
teamId,
109+
userEmail: (request.user as any).email
110+
}, 'Team deletion email queued');
111+
}
112+
} catch (emailError) {
113+
server.log.error(emailError, 'Failed to queue team deletion email - team deletion succeeded but email not sent');
114+
// Don't fail team deletion if email queueing fails
115+
}
91116

92117
const successResponse: DeleteSuccessResponse = {
93118
success: true,

services/backend/src/services/teamService.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { eq, and, count } from 'drizzle-orm';
2+
import { eq, and, count, or } from 'drizzle-orm';
33
import { getDb, getSchema } from '../db/index';
44
import { generateId } from 'lucia';
55
import { GlobalSettings } from '../global-settings/helpers';
66
import type { FastifyBaseLogger } from 'fastify';
7+
import { McpInstallationService } from './mcpInstallationService';
8+
import { SatelliteCommandService } from './satelliteCommandService';
79

810
export interface Team {
911
id: string;
@@ -281,9 +283,99 @@ export class TeamService {
281283
/**
282284
* Delete team
283285
*/
284-
static async deleteTeam(teamId: string): Promise<boolean> {
286+
static async deleteTeam(teamId: string, userId: string, logger: FastifyBaseLogger): Promise<boolean> {
285287
const { db, schema } = this.getDbAndSchema();
286-
// Delete team memberships first (cascade should handle this, but being explicit)
288+
289+
// Delete all MCP installations for this team
290+
// This will remove the database records and create satellite commands to kill processes
291+
try {
292+
const installationService = new McpInstallationService(db, logger);
293+
const satelliteCommandService = new SatelliteCommandService(db, logger);
294+
295+
// Get all MCP installations for this team
296+
const installations = await installationService.getTeamInstallations(teamId, userId);
297+
298+
let deletedCount = 0;
299+
let totalCommands = 0;
300+
301+
// Delete each installation and create satellite commands
302+
for (const installation of installations) {
303+
try {
304+
// 1. Delete installation from database
305+
const deleted = await installationService.deleteInstallation(installation.id, teamId);
306+
if (deleted) {
307+
deletedCount++;
308+
}
309+
310+
// 2. Create satellite commands to kill processes (fire-and-forget)
311+
try {
312+
const commands = await satelliteCommandService.notifyMcpInstallation(
313+
installation.id,
314+
teamId,
315+
userId
316+
);
317+
totalCommands += commands.length;
318+
} catch (commandError) {
319+
logger.error(commandError, `Failed to create satellite commands for installation ${installation.id}`);
320+
// Continue even if satellite command creation fails
321+
}
322+
} catch (installationError) {
323+
logger.error(installationError, `Failed to delete installation ${installation.id}`);
324+
// Continue with other installations even if one fails
325+
}
326+
}
327+
328+
logger.info({
329+
operation: 'team_deletion',
330+
teamId,
331+
totalInstallations: installations.length,
332+
installationsDeleted: deletedCount,
333+
satelliteCommandsCreated: totalCommands
334+
}, 'MCP installations deleted and satellite commands created for team deletion');
335+
336+
} catch (error) {
337+
logger.error(error, 'Failed to delete MCP installations - proceeding with team deletion anyway');
338+
// Don't fail team deletion if installation deletion fails
339+
}
340+
341+
// Handle satellite commands - preserve pending commands so satellites can pick them up
342+
// Set target_team_id to NULL for pending commands (allows team deletion without blocking satellite execution)
343+
await (db as any)
344+
.update(schema.satelliteCommands)
345+
.set({ target_team_id: null })
346+
.where(
347+
and(
348+
eq(schema.satelliteCommands.target_team_id, teamId),
349+
eq(schema.satelliteCommands.status, 'pending')
350+
)
351+
);
352+
353+
// Delete non-pending satellite commands (completed/failed/executing) since they're no longer needed
354+
await (db as any)
355+
.delete(schema.satelliteCommands)
356+
.where(
357+
and(
358+
eq(schema.satelliteCommands.target_team_id, teamId),
359+
or(
360+
eq(schema.satelliteCommands.status, 'completed'),
361+
eq(schema.satelliteCommands.status, 'failed'),
362+
eq(schema.satelliteCommands.status, 'acknowledged'),
363+
eq(schema.satelliteCommands.status, 'executing')
364+
)
365+
)
366+
);
367+
368+
// Delete satellite processes for this team
369+
await (db as any)
370+
.delete(schema.satelliteProcesses)
371+
.where(eq(schema.satelliteProcesses.team_id, teamId));
372+
373+
// Delete satellite usage logs for this team
374+
await (db as any)
375+
.delete(schema.satelliteUsageLogs)
376+
.where(eq(schema.satelliteUsageLogs.team_id, teamId));
377+
378+
// Delete team memberships (cascade should handle this, but being explicit)
287379
await (db as any)
288380
.delete(schema.teamMemberships)
289381
.where(eq(schema.teamMemberships.team_id, teamId));

0 commit comments

Comments
 (0)