Skip to content

Commit e35ed2d

Browse files
author
Lasim
committed
feat(backend): add event emissions for user and MCP server actions
1 parent 3bbfbf5 commit e35ed2d

File tree

14 files changed

+589
-3
lines changed

14 files changed

+589
-3
lines changed

services/backend/src/events/eventNames.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export const EVENT_NAMES = {
3535
MCP_INSTALLATION_CREATED: 'mcp.installation_created', // matches mcp.installations.create
3636
MCP_INSTALLATION_UPDATED: 'mcp.installation_updated', // matches mcp.installations.edit
3737
MCP_INSTALLATION_DELETED: 'mcp.installation_deleted', // matches mcp.installations.delete
38+
MCP_SERVER_CREATED: 'mcp.server_created', // matches mcp.servers.create
39+
MCP_SERVER_UPDATED: 'mcp.server_updated', // matches mcp.servers.edit
40+
MCP_SERVER_DELETED: 'mcp.server_deleted', // matches mcp.servers.delete
3841

3942
// System Events
4043
SYSTEM_STARTUP: 'system.startup',

services/backend/src/events/types.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,56 @@ export interface CoreEventData {
337337
};
338338
};
339339

340+
[EVENT_NAMES.MCP_SERVER_CREATED]: {
341+
server: {
342+
id: string;
343+
name: string;
344+
description?: string;
345+
language: string;
346+
runtime: string;
347+
};
348+
createdBy: {
349+
id: string;
350+
email: string;
351+
};
352+
metadata: {
353+
ip: string;
354+
};
355+
};
356+
357+
[EVENT_NAMES.MCP_SERVER_UPDATED]: {
358+
server: {
359+
id: string;
360+
name: string;
361+
description?: string;
362+
language: string;
363+
runtime: string;
364+
};
365+
updatedBy: {
366+
id: string;
367+
email: string;
368+
};
369+
changes: Record<string, any>;
370+
metadata: {
371+
ip: string;
372+
};
373+
};
374+
375+
[EVENT_NAMES.MCP_SERVER_DELETED]: {
376+
server: {
377+
id: string;
378+
name: string;
379+
description?: string;
380+
};
381+
deletedBy: {
382+
id: string;
383+
email: string;
384+
};
385+
metadata: {
386+
ip: string;
387+
};
388+
};
389+
340390
[EVENT_NAMES.SYSTEM_STARTUP]: {
341391
version: string;
342392
environment: string;

services/backend/src/routes/auth/loginEmail.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { verify } from '@node-rs/argon2';
44
import { getDb, getSchema } from '../../db';
55
import { eq, or } from 'drizzle-orm';
66
import { GlobalSettingsInitService } from '../../global-settings';
7+
import { EVENT_NAMES } from '../../events';
8+
import type { EventContext } from '../../events/types';
79

810
// Reusable Schema Constants
911
const LOGIN_REQUEST_SCHEMA = {
@@ -287,6 +289,46 @@ export default async function loginEmailRoute(server: FastifyInstance) {
287289

288290
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
289291

292+
// Emit USER_LOGIN event
293+
try {
294+
const eventContext: EventContext = {
295+
db,
296+
logger: server.log,
297+
user: {
298+
id: user.id,
299+
email: user.email,
300+
roleId: user.role_id
301+
},
302+
request: {
303+
ip: request.ip,
304+
userAgent: request.headers['user-agent'],
305+
requestId: request.id
306+
},
307+
timestamp: new Date()
308+
};
309+
310+
server.eventBus.emitWithContext(
311+
EVENT_NAMES.USER_LOGIN,
312+
{
313+
user: {
314+
id: user.id,
315+
email: user.email,
316+
name: user.username || `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email
317+
},
318+
metadata: {
319+
loginMethod: 'email',
320+
ip: request.ip,
321+
userAgent: request.headers['user-agent']
322+
}
323+
},
324+
eventContext
325+
);
326+
server.log.info(`USER_LOGIN event emitted for user: ${user.id}`);
327+
} catch (eventError) {
328+
server.log.error(eventError, `Failed to emit USER_LOGIN event for user ${user.id}:`);
329+
// Don't fail login if event emission fails
330+
}
331+
290332
// Create clean response object to avoid serialization issues
291333
const cleanResponse: LoginSuccessResponse = {
292334
success: true,

services/backend/src/routes/auth/logout.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
23
import { getLucia } from '../../lib/lucia';
34
import { getDb, getSchema, getDbStatus } from '../../db';
45
import { eq } from 'drizzle-orm';
56
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
67
import { z } from 'zod';
78
import { createSchema } from 'zod-openapi';
9+
import { EVENT_NAMES } from '../../events';
10+
import type { EventContext } from '../../events/types';
811

912
// Zod schema for the logout response
1013
const logoutResponseSchema = z.object({
@@ -58,7 +61,7 @@ export default async function logoutRoute(fastify: FastifyInstance) {
5861
if (authSessionTable && authSessionTable.id) {
5962
const dbStatus = getDbStatus();
6063
if (dbStatus.dialect === 'sqlite') {
61-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
64+
6265
const sqliteDb = db as BetterSQLite3Database<any>;
6366
await sqliteDb.delete(authSessionTable).where(eq(authSessionTable.id, sessionId));
6467
}
@@ -94,6 +97,67 @@ export default async function logoutRoute(fastify: FastifyInstance) {
9497
reply.setCookie(blankCookie.name, blankCookie.value, blankCookie.attributes);
9598
fastify.log.info('Blank cookie sent to clear client session');
9699

100+
// Emit USER_LOGOUT event
101+
try {
102+
if (request.user) {
103+
// Get user role from database since it's not in the session
104+
const db = getDb();
105+
const schema = getSchema();
106+
const authUserTable = schema.authUser;
107+
let userRole = 'unknown';
108+
109+
try {
110+
const userResult = await (db as any)
111+
.select({ role_id: authUserTable.role_id })
112+
.from(authUserTable)
113+
.where(eq(authUserTable.id, request.user.id))
114+
.limit(1);
115+
116+
if (userResult.length > 0) {
117+
userRole = userResult[0].role_id;
118+
}
119+
} catch (roleError) {
120+
fastify.log.warn(roleError, 'Failed to fetch user role for logout event');
121+
}
122+
123+
const eventContext: EventContext = {
124+
db,
125+
logger: fastify.log,
126+
user: {
127+
id: request.user.id,
128+
email: (request.user as any).email,
129+
roleId: userRole
130+
},
131+
request: {
132+
ip: request.ip,
133+
userAgent: request.headers['user-agent'],
134+
requestId: request.id
135+
},
136+
timestamp: new Date()
137+
};
138+
139+
fastify.eventBus.emitWithContext(
140+
EVENT_NAMES.USER_LOGOUT,
141+
{
142+
user: {
143+
id: request.user.id,
144+
email: (request.user as any).email,
145+
name: (request.user as any).username || `${(request.user as any).firstName || ''} ${(request.user as any).lastName || ''}`.trim() || (request.user as any).email
146+
},
147+
metadata: {
148+
ip: request.ip,
149+
userAgent: request.headers['user-agent']
150+
}
151+
},
152+
eventContext
153+
);
154+
fastify.log.info(`USER_LOGOUT event emitted for user: ${request.user.id}`);
155+
}
156+
} catch (eventError) {
157+
fastify.log.error(eventError, 'Failed to emit USER_LOGOUT event:');
158+
// Don't fail logout if event emission fails
159+
}
160+
97161
const response = { success: true, message: 'Logged out successfully.' };
98162
const jsonString = JSON.stringify(response);
99163
return reply.status(200).type('application/json').send(jsonString);
@@ -112,7 +176,7 @@ export default async function logoutRoute(fastify: FastifyInstance) {
112176
if (authSessionTable && authSessionTable.id) {
113177
const dbStatus = getDbStatus();
114178
if (dbStatus.dialect === 'sqlite') {
115-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
179+
116180
const sqliteDb = db as BetterSQLite3Database<any>;
117181
await sqliteDb.delete(authSessionTable).where(eq(authSessionTable.id, sessionId));
118182
}

services/backend/src/routes/auth/registerEmail.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { hash } from '@node-rs/argon2';
77
import { TeamService } from '../../services/teamService';
88
import { GlobalSettingsInitService } from '../../global-settings';
99
import { UserPreferencesService } from '../../services/UserPreferencesService';
10+
import { EVENT_NAMES } from '../../events';
11+
import type { EventContext } from '../../events/types';
1012

1113
// Reusable Schema Constants
1214
const REGISTER_EMAIL_REQUEST_SCHEMA = {
@@ -347,6 +349,47 @@ export default async function registerEmailRoute(server: FastifyInstance) {
347349
// Get the created user data
348350
const user = createdUser[0];
349351

352+
// Emit USER_REGISTERED event
353+
try {
354+
const eventContext: EventContext = {
355+
db,
356+
logger: server.log,
357+
user: {
358+
id: user.id,
359+
email: user.email,
360+
roleId: user.role_id
361+
},
362+
request: {
363+
ip: request.ip,
364+
userAgent: request.headers['user-agent'],
365+
requestId: request.id
366+
},
367+
timestamp: new Date()
368+
};
369+
370+
server.eventBus.emitWithContext(
371+
EVENT_NAMES.USER_REGISTERED,
372+
{
373+
user: {
374+
id: user.id,
375+
email: user.email,
376+
name: user.username || `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email,
377+
createdAt: new Date()
378+
},
379+
metadata: {
380+
registrationMethod: 'email',
381+
ip: request.ip,
382+
userAgent: request.headers['user-agent']
383+
}
384+
},
385+
eventContext
386+
);
387+
server.log.info(`USER_REGISTERED event emitted for user: ${user.id}`);
388+
} catch (eventError) {
389+
server.log.error(eventError, `Failed to emit USER_REGISTERED event for user ${user.id}:`);
390+
// Don't fail registration if event emission fails
391+
}
392+
350393
// Customize message based on email verification status
351394
let message = 'User registered successfully.';
352395
if (isFirstUser) {

services/backend/src/routes/mcp/installations/create.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
type InstallationSuccessResponse,
1717
type ErrorResponse
1818
} from './schemas';
19+
import { EVENT_NAMES } from '../../../events';
20+
import type { EventContext } from '../../../events/types';
1921

2022
export default async function createInstallationRoute(server: FastifyInstance) {
2123
server.post('/teams/:teamId/mcp/installations', {
@@ -99,6 +101,50 @@ export default async function createInstallationRoute(server: FastifyInstance) {
99101
serverId: installationData.server_id
100102
}, 'MCP server installation created successfully');
101103

104+
// Emit MCP_INSTALLATION_CREATED event
105+
try {
106+
const eventContext: EventContext = {
107+
db,
108+
logger: request.log,
109+
user: {
110+
id: userId,
111+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
112+
email: (request.user as any).email,
113+
roleId: 'unknown'
114+
},
115+
request: {
116+
ip: request.ip,
117+
userAgent: request.headers['user-agent'],
118+
requestId: request.id
119+
},
120+
timestamp: new Date()
121+
};
122+
123+
server.eventBus.emitWithContext(
124+
EVENT_NAMES.MCP_INSTALLATION_CREATED,
125+
{
126+
installation: {
127+
id: installation.id,
128+
serverId: installation.server_id,
129+
teamId: teamId
130+
},
131+
installedBy: {
132+
id: userId,
133+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
134+
email: (request.user as any).email
135+
},
136+
metadata: {
137+
ip: request.ip
138+
}
139+
},
140+
eventContext
141+
);
142+
request.log.info(`MCP_INSTALLATION_CREATED event emitted for installation: ${installation.id}`);
143+
} catch (eventError) {
144+
request.log.error(eventError, `Failed to emit MCP_INSTALLATION_CREATED event for installation ${installation.id}:`);
145+
// Don't fail installation creation if event emission fails
146+
}
147+
102148
const response: InstallationSuccessResponse = {
103149
success: true,
104150
data: formatInstallationResponse(installation)

0 commit comments

Comments
 (0)