Skip to content

Commit 481ce1e

Browse files
author
Lasim
committed
feat(backend): implement device activity tracking service and integrate with MCP configurations route
1 parent 80dd6f9 commit 481ce1e

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed

services/backend/src/routes/gateway/me-mcp-configurations.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { eq, and, inArray } from 'drizzle-orm';
55
import { mcpServers, mcpServerInstallations, mcpUserConfigurations, teamMemberships, devices } from '../../db/schema.sqlite';
66
import { McpArgsStorage } from '../../utils/mcpArgsStorage';
77
import { McpEnvStorage } from '../../utils/mcpEnvStorage';
8+
import { trackDeviceActivity } from '../../services/deviceActivityService';
89

910
// Response schemas
1011
const GATEWAY_MCP_SERVER_SCHEMA = {
@@ -447,6 +448,14 @@ export default async function gatewayMeMcpConfigurationsRoute(server: FastifyIns
447448
invalidServers: servers.filter(s => s.status === 'invalid').length
448449
}, 'Successfully retrieved gateway MCP configurations');
449450

451+
// Track device activity if hardware_id was provided (fire-and-forget)
452+
if (hardwareId) {
453+
trackDeviceActivity(db, hardwareId, request.log, {
454+
updateLastIp: request.ip,
455+
silent: true
456+
});
457+
}
458+
450459
const successResponse: SuccessResponse = {
451460
success: true,
452461
data: {
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { eq } from 'drizzle-orm';
2+
import { devices } from '../db/schema.sqlite';
3+
import type { AnyDatabase } from '../db';
4+
import type { FastifyBaseLogger } from 'fastify';
5+
6+
/**
7+
* Service for tracking device activity by updating last_activity_at timestamps
8+
* Used across different API endpoints to maintain enterprise device management features
9+
*/
10+
export class DeviceActivityService {
11+
private db: AnyDatabase;
12+
private logger: FastifyBaseLogger;
13+
14+
constructor(db: AnyDatabase, logger: FastifyBaseLogger) {
15+
this.db = db;
16+
this.logger = logger.child({ service: 'DeviceActivityService' });
17+
}
18+
19+
/**
20+
* Updates the last_activity_at timestamp for a device identified by hardware_id
21+
* This method is designed to be non-blocking and should not throw errors that affect API responses
22+
*
23+
* @param hardwareId - The unique hardware fingerprint of the device
24+
* @param options - Optional configuration
25+
* @returns Promise<boolean> - true if update was successful, false otherwise
26+
*/
27+
async updateDeviceActivity(
28+
hardwareId: string,
29+
options: {
30+
updateLastIp?: string;
31+
silent?: boolean; // If true, don't log errors
32+
} = {}
33+
): Promise<boolean> {
34+
try {
35+
if (!hardwareId || typeof hardwareId !== 'string') {
36+
if (!options.silent) {
37+
this.logger.warn({
38+
operation: 'update_device_activity',
39+
hardwareId,
40+
error: 'invalid_hardware_id'
41+
}, 'Invalid hardware_id provided');
42+
}
43+
return false;
44+
}
45+
46+
const now = new Date();
47+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
48+
const updateData: any = {
49+
last_activity_at: now,
50+
updated_at: now,
51+
};
52+
53+
// Optionally update last known IP address
54+
if (options.updateLastIp) {
55+
updateData.last_ip = options.updateLastIp;
56+
}
57+
58+
const result = await this.db
59+
.update(devices)
60+
.set(updateData)
61+
.where(eq(devices.hardware_id, hardwareId))
62+
.run();
63+
64+
// Check if any rows were affected (device was found and updated)
65+
const success = result.changes > 0;
66+
67+
if (!success && !options.silent) {
68+
this.logger.warn({
69+
operation: 'update_device_activity',
70+
hardwareId,
71+
error: 'device_not_found'
72+
}, 'No device found with hardware_id');
73+
}
74+
75+
return success;
76+
} catch (error) {
77+
if (!options.silent) {
78+
this.logger.error({
79+
operation: 'update_device_activity',
80+
hardwareId,
81+
error,
82+
updateLastIp: options.updateLastIp
83+
}, 'Failed to update device activity');
84+
}
85+
return false;
86+
}
87+
}
88+
89+
/**
90+
* Updates device activity and also sets last_login_at (for login scenarios)
91+
*
92+
* @param hardwareId - The unique hardware fingerprint of the device
93+
* @param options - Optional configuration
94+
* @returns Promise<boolean> - true if update was successful, false otherwise
95+
*/
96+
async updateDeviceLogin(
97+
hardwareId: string,
98+
options: {
99+
updateLastIp?: string;
100+
silent?: boolean;
101+
} = {}
102+
): Promise<boolean> {
103+
try {
104+
if (!hardwareId || typeof hardwareId !== 'string') {
105+
if (!options.silent) {
106+
this.logger.warn({
107+
operation: 'update_device_login',
108+
hardwareId,
109+
error: 'invalid_hardware_id'
110+
}, 'Invalid hardware_id provided');
111+
}
112+
return false;
113+
}
114+
115+
const now = new Date();
116+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
117+
const updateData: any = {
118+
last_activity_at: now,
119+
last_login_at: now,
120+
updated_at: now,
121+
};
122+
123+
// Optionally update last known IP address
124+
if (options.updateLastIp) {
125+
updateData.last_ip = options.updateLastIp;
126+
}
127+
128+
const result = await this.db
129+
.update(devices)
130+
.set(updateData)
131+
.where(eq(devices.hardware_id, hardwareId))
132+
.run();
133+
134+
const success = result.changes > 0;
135+
136+
if (!success && !options.silent) {
137+
this.logger.warn({
138+
operation: 'update_device_login',
139+
hardwareId,
140+
error: 'device_not_found'
141+
}, 'No device found with hardware_id');
142+
}
143+
144+
return success;
145+
} catch (error) {
146+
if (!options.silent) {
147+
this.logger.error({
148+
operation: 'update_device_login',
149+
hardwareId,
150+
error,
151+
updateLastIp: options.updateLastIp
152+
}, 'Failed to update device login activity');
153+
}
154+
return false;
155+
}
156+
}
157+
158+
/**
159+
* Gets device information by hardware_id (useful for debugging)
160+
*
161+
* @param hardwareId - The unique hardware fingerprint of the device
162+
* @returns Promise<Device | null> - Device record or null if not found
163+
*/
164+
async getDeviceByHardwareId(hardwareId: string) {
165+
try {
166+
if (!hardwareId || typeof hardwareId !== 'string') {
167+
return null;
168+
}
169+
170+
const device = await this.db
171+
.select()
172+
.from(devices)
173+
.where(eq(devices.hardware_id, hardwareId))
174+
.get();
175+
176+
return device || null;
177+
} catch (error) {
178+
this.logger.error({
179+
operation: 'get_device_by_hardware_id',
180+
hardwareId,
181+
error
182+
}, 'Failed to get device by hardware_id');
183+
return null;
184+
}
185+
}
186+
}
187+
188+
/**
189+
* Utility function to create a DeviceActivityService instance
190+
* Can be used in route handlers for quick access
191+
*
192+
* @param db - Database instance
193+
* @param logger - Fastify logger instance
194+
* @returns DeviceActivityService instance
195+
*/
196+
export function createDeviceActivityService(db: AnyDatabase, logger: FastifyBaseLogger): DeviceActivityService {
197+
return new DeviceActivityService(db, logger);
198+
}
199+
200+
/**
201+
* Helper function for fire-and-forget device activity updates
202+
* Use this in API endpoints where you don't want to wait for the update to complete
203+
*
204+
* @param db - Database instance
205+
* @param hardwareId - The unique hardware fingerprint of the device
206+
* @param logger - Fastify logger instance
207+
* @param options - Optional configuration
208+
*/
209+
export async function trackDeviceActivity(
210+
db: AnyDatabase,
211+
hardwareId: string,
212+
logger: FastifyBaseLogger,
213+
options: {
214+
updateLastIp?: string;
215+
silent?: boolean;
216+
} = {}
217+
): Promise<void> {
218+
// Fire and forget - don't await this
219+
const service = new DeviceActivityService(db, logger);
220+
service.updateDeviceActivity(hardwareId, { ...options, silent: true }).catch(() => {
221+
// Silently ignore errors in fire-and-forget mode
222+
});
223+
}

0 commit comments

Comments
 (0)