@openmdm/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +105 -3
- package/dist/index.js +1553 -41
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +259 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +591 -1
- package/dist/types.js +21 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/audit.ts +317 -0
- package/src/authorization.ts +418 -0
- package/src/dashboard.ts +327 -0
- package/src/index.ts +222 -0
- package/src/plugin-storage.ts +128 -0
- package/src/queue.ts +161 -0
- package/src/schedule.ts +325 -0
- package/src/schema.ts +277 -0
- package/src/tenant.ts +237 -0
- package/src/types.ts +708 -0
package/dist/index.js
CHANGED
|
@@ -27,6 +27,26 @@ var ApplicationNotFoundError = class extends MDMError {
|
|
|
27
27
|
super(`Application not found: ${identifier}`, "APPLICATION_NOT_FOUND", 404);
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
|
+
var TenantNotFoundError = class extends MDMError {
|
|
31
|
+
constructor(identifier) {
|
|
32
|
+
super(`Tenant not found: ${identifier}`, "TENANT_NOT_FOUND", 404);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var RoleNotFoundError = class extends MDMError {
|
|
36
|
+
constructor(identifier) {
|
|
37
|
+
super(`Role not found: ${identifier}`, "ROLE_NOT_FOUND", 404);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var GroupNotFoundError = class extends MDMError {
|
|
41
|
+
constructor(identifier) {
|
|
42
|
+
super(`Group not found: ${identifier}`, "GROUP_NOT_FOUND", 404);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var UserNotFoundError = class extends MDMError {
|
|
46
|
+
constructor(identifier) {
|
|
47
|
+
super(`User not found: ${identifier}`, "USER_NOT_FOUND", 404);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
30
50
|
var EnrollmentError = class extends MDMError {
|
|
31
51
|
constructor(message, details) {
|
|
32
52
|
super(message, "ENROLLMENT_ERROR", 400, details);
|
|
@@ -162,56 +182,1170 @@ function createWebhookManager(config) {
|
|
|
162
182
|
);
|
|
163
183
|
}
|
|
164
184
|
}
|
|
165
|
-
return results;
|
|
185
|
+
return results;
|
|
186
|
+
},
|
|
187
|
+
addEndpoint(endpoint) {
|
|
188
|
+
endpoints.set(endpoint.id, endpoint);
|
|
189
|
+
},
|
|
190
|
+
removeEndpoint(endpointId) {
|
|
191
|
+
endpoints.delete(endpointId);
|
|
192
|
+
},
|
|
193
|
+
updateEndpoint(endpointId, updates) {
|
|
194
|
+
const existing = endpoints.get(endpointId);
|
|
195
|
+
if (existing) {
|
|
196
|
+
endpoints.set(endpointId, { ...existing, ...updates });
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
getEndpoints() {
|
|
200
|
+
return Array.from(endpoints.values());
|
|
201
|
+
},
|
|
202
|
+
async testEndpoint(endpointId) {
|
|
203
|
+
const endpoint = endpoints.get(endpointId);
|
|
204
|
+
if (!endpoint) {
|
|
205
|
+
return {
|
|
206
|
+
endpointId,
|
|
207
|
+
success: false,
|
|
208
|
+
error: "Endpoint not found",
|
|
209
|
+
retryCount: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const testPayload = {
|
|
213
|
+
id: randomUUID(),
|
|
214
|
+
event: "device.heartbeat",
|
|
215
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
216
|
+
data: {
|
|
217
|
+
test: true,
|
|
218
|
+
message: "OpenMDM webhook test"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
return deliverToEndpoint(endpoint, testPayload);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
226
|
+
const expectedSignature = `sha256=${createHmac("sha256", secret).update(payload).digest("hex")}`;
|
|
227
|
+
if (signature.length !== expectedSignature.length) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
let result = 0;
|
|
231
|
+
for (let i = 0; i < signature.length; i++) {
|
|
232
|
+
result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
|
|
233
|
+
}
|
|
234
|
+
return result === 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/tenant.ts
|
|
238
|
+
function validateSlug(slug) {
|
|
239
|
+
const slugRegex = /^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/;
|
|
240
|
+
return slugRegex.test(slug);
|
|
241
|
+
}
|
|
242
|
+
function createTenantManager(db) {
|
|
243
|
+
return {
|
|
244
|
+
async get(id) {
|
|
245
|
+
if (!db.findTenant) {
|
|
246
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
247
|
+
}
|
|
248
|
+
return db.findTenant(id);
|
|
249
|
+
},
|
|
250
|
+
async getBySlug(slug) {
|
|
251
|
+
if (!db.findTenantBySlug) {
|
|
252
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
253
|
+
}
|
|
254
|
+
return db.findTenantBySlug(slug);
|
|
255
|
+
},
|
|
256
|
+
async list(filter) {
|
|
257
|
+
if (!db.listTenants) {
|
|
258
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
259
|
+
}
|
|
260
|
+
return db.listTenants(filter);
|
|
261
|
+
},
|
|
262
|
+
async create(data) {
|
|
263
|
+
if (!db.createTenant || !db.findTenantBySlug) {
|
|
264
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
265
|
+
}
|
|
266
|
+
if (!validateSlug(data.slug)) {
|
|
267
|
+
throw new ValidationError(
|
|
268
|
+
"Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.",
|
|
269
|
+
{ slug: data.slug }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const existing = await db.findTenantBySlug(data.slug);
|
|
273
|
+
if (existing) {
|
|
274
|
+
throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
|
|
275
|
+
slug: data.slug
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return db.createTenant({
|
|
279
|
+
...data,
|
|
280
|
+
slug: data.slug.toLowerCase()
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
async update(id, data) {
|
|
284
|
+
if (!db.updateTenant || !db.findTenant || !db.findTenantBySlug) {
|
|
285
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
286
|
+
}
|
|
287
|
+
const tenant = await db.findTenant(id);
|
|
288
|
+
if (!tenant) {
|
|
289
|
+
throw new TenantNotFoundError(id);
|
|
290
|
+
}
|
|
291
|
+
if (data.slug) {
|
|
292
|
+
if (!validateSlug(data.slug)) {
|
|
293
|
+
throw new ValidationError(
|
|
294
|
+
"Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.",
|
|
295
|
+
{ slug: data.slug }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const existing = await db.findTenantBySlug(data.slug);
|
|
299
|
+
if (existing && existing.id !== id) {
|
|
300
|
+
throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
|
|
301
|
+
slug: data.slug
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
data.slug = data.slug.toLowerCase();
|
|
305
|
+
}
|
|
306
|
+
return db.updateTenant(id, data);
|
|
307
|
+
},
|
|
308
|
+
async delete(id, cascade = false) {
|
|
309
|
+
if (!db.deleteTenant || !db.findTenant) {
|
|
310
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
311
|
+
}
|
|
312
|
+
const tenant = await db.findTenant(id);
|
|
313
|
+
if (!tenant) {
|
|
314
|
+
throw new TenantNotFoundError(id);
|
|
315
|
+
}
|
|
316
|
+
await db.deleteTenant(id);
|
|
317
|
+
},
|
|
318
|
+
async getStats(tenantId) {
|
|
319
|
+
if (!db.getTenantStats || !db.findTenant) {
|
|
320
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
321
|
+
}
|
|
322
|
+
const tenant = await db.findTenant(tenantId);
|
|
323
|
+
if (!tenant) {
|
|
324
|
+
throw new TenantNotFoundError(tenantId);
|
|
325
|
+
}
|
|
326
|
+
return db.getTenantStats(tenantId);
|
|
327
|
+
},
|
|
328
|
+
async activate(id) {
|
|
329
|
+
if (!db.updateTenant || !db.findTenant) {
|
|
330
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
331
|
+
}
|
|
332
|
+
const tenant = await db.findTenant(id);
|
|
333
|
+
if (!tenant) {
|
|
334
|
+
throw new TenantNotFoundError(id);
|
|
335
|
+
}
|
|
336
|
+
if (tenant.status === "active") {
|
|
337
|
+
return tenant;
|
|
338
|
+
}
|
|
339
|
+
return db.updateTenant(id, { status: "active" });
|
|
340
|
+
},
|
|
341
|
+
async deactivate(id) {
|
|
342
|
+
if (!db.updateTenant || !db.findTenant) {
|
|
343
|
+
throw new Error("Database adapter does not support tenant operations");
|
|
344
|
+
}
|
|
345
|
+
const tenant = await db.findTenant(id);
|
|
346
|
+
if (!tenant) {
|
|
347
|
+
throw new TenantNotFoundError(id);
|
|
348
|
+
}
|
|
349
|
+
if (tenant.status === "suspended") {
|
|
350
|
+
return tenant;
|
|
351
|
+
}
|
|
352
|
+
return db.updateTenant(id, { status: "suspended" });
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/authorization.ts
|
|
358
|
+
function actionMatches(required, granted) {
|
|
359
|
+
if (granted === "*") return true;
|
|
360
|
+
if (granted === "manage") {
|
|
361
|
+
return ["create", "read", "update", "delete", "manage"].includes(required);
|
|
362
|
+
}
|
|
363
|
+
return required === granted;
|
|
364
|
+
}
|
|
365
|
+
function resourceMatches(required, granted) {
|
|
366
|
+
if (granted === "*") return true;
|
|
367
|
+
return required === granted;
|
|
368
|
+
}
|
|
369
|
+
function permissionMatches(required, granted) {
|
|
370
|
+
return actionMatches(required.action, granted.action) && resourceMatches(required.resource, granted.resource);
|
|
371
|
+
}
|
|
372
|
+
function hasPermission(permissions, action, resource) {
|
|
373
|
+
return permissions.some((p) => permissionMatches({ action, resource }, p));
|
|
374
|
+
}
|
|
375
|
+
function isAdminPermission(permissions) {
|
|
376
|
+
return permissions.some(
|
|
377
|
+
(p) => p.action === "*" && p.resource === "*"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function validateEmail(email) {
|
|
381
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
382
|
+
return emailRegex.test(email);
|
|
383
|
+
}
|
|
384
|
+
function createAuthorizationManager(db) {
|
|
385
|
+
async function getAllUserPermissions(userId) {
|
|
386
|
+
if (!db.getUserRoles) {
|
|
387
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
388
|
+
}
|
|
389
|
+
const roles = await db.getUserRoles(userId);
|
|
390
|
+
const permissions = [];
|
|
391
|
+
for (const role of roles) {
|
|
392
|
+
permissions.push(...role.permissions);
|
|
393
|
+
}
|
|
394
|
+
return permissions;
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
// ========================================
|
|
398
|
+
// Role Management
|
|
399
|
+
// ========================================
|
|
400
|
+
async createRole(data) {
|
|
401
|
+
if (!db.createRole) {
|
|
402
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
403
|
+
}
|
|
404
|
+
if (!data.permissions || !Array.isArray(data.permissions)) {
|
|
405
|
+
throw new ValidationError("Permissions must be an array");
|
|
406
|
+
}
|
|
407
|
+
for (const permission of data.permissions) {
|
|
408
|
+
if (!permission.action || !permission.resource) {
|
|
409
|
+
throw new ValidationError("Each permission must have action and resource");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return db.createRole(data);
|
|
413
|
+
},
|
|
414
|
+
async getRole(id) {
|
|
415
|
+
if (!db.findRole) {
|
|
416
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
417
|
+
}
|
|
418
|
+
return db.findRole(id);
|
|
419
|
+
},
|
|
420
|
+
async listRoles(tenantId) {
|
|
421
|
+
if (!db.listRoles) {
|
|
422
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
423
|
+
}
|
|
424
|
+
return db.listRoles(tenantId);
|
|
425
|
+
},
|
|
426
|
+
async updateRole(id, data) {
|
|
427
|
+
if (!db.updateRole || !db.findRole) {
|
|
428
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
429
|
+
}
|
|
430
|
+
const role = await db.findRole(id);
|
|
431
|
+
if (!role) {
|
|
432
|
+
throw new RoleNotFoundError(id);
|
|
433
|
+
}
|
|
434
|
+
if (role.isSystem) {
|
|
435
|
+
throw new AuthorizationError("Cannot modify system roles");
|
|
436
|
+
}
|
|
437
|
+
if (data.permissions) {
|
|
438
|
+
if (!Array.isArray(data.permissions)) {
|
|
439
|
+
throw new ValidationError("Permissions must be an array");
|
|
440
|
+
}
|
|
441
|
+
for (const permission of data.permissions) {
|
|
442
|
+
if (!permission.action || !permission.resource) {
|
|
443
|
+
throw new ValidationError("Each permission must have action and resource");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return db.updateRole(id, data);
|
|
448
|
+
},
|
|
449
|
+
async deleteRole(id) {
|
|
450
|
+
if (!db.deleteRole || !db.findRole) {
|
|
451
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
452
|
+
}
|
|
453
|
+
const role = await db.findRole(id);
|
|
454
|
+
if (!role) {
|
|
455
|
+
throw new RoleNotFoundError(id);
|
|
456
|
+
}
|
|
457
|
+
if (role.isSystem) {
|
|
458
|
+
throw new AuthorizationError("Cannot delete system roles");
|
|
459
|
+
}
|
|
460
|
+
await db.deleteRole(id);
|
|
461
|
+
},
|
|
462
|
+
// ========================================
|
|
463
|
+
// User Management
|
|
464
|
+
// ========================================
|
|
465
|
+
async createUser(data) {
|
|
466
|
+
if (!db.createUser || !db.findUserByEmail) {
|
|
467
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
468
|
+
}
|
|
469
|
+
if (!validateEmail(data.email)) {
|
|
470
|
+
throw new ValidationError("Invalid email format", { email: data.email });
|
|
471
|
+
}
|
|
472
|
+
const existing = await db.findUserByEmail(data.email, data.tenantId);
|
|
473
|
+
if (existing) {
|
|
474
|
+
throw new ValidationError(`User with email '${data.email}' already exists`, {
|
|
475
|
+
email: data.email
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return db.createUser({
|
|
479
|
+
...data,
|
|
480
|
+
email: data.email.toLowerCase()
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
async getUser(id) {
|
|
484
|
+
if (!db.findUser || !db.getUserRoles) {
|
|
485
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
486
|
+
}
|
|
487
|
+
const user = await db.findUser(id);
|
|
488
|
+
if (!user) return null;
|
|
489
|
+
const roles = await db.getUserRoles(id);
|
|
490
|
+
return { ...user, roles };
|
|
491
|
+
},
|
|
492
|
+
async getUserByEmail(email, tenantId) {
|
|
493
|
+
if (!db.findUserByEmail || !db.getUserRoles) {
|
|
494
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
495
|
+
}
|
|
496
|
+
const user = await db.findUserByEmail(email.toLowerCase(), tenantId);
|
|
497
|
+
if (!user) return null;
|
|
498
|
+
const roles = await db.getUserRoles(user.id);
|
|
499
|
+
return { ...user, roles };
|
|
500
|
+
},
|
|
501
|
+
async listUsers(filter) {
|
|
502
|
+
if (!db.listUsers) {
|
|
503
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
504
|
+
}
|
|
505
|
+
return db.listUsers(filter);
|
|
506
|
+
},
|
|
507
|
+
async updateUser(id, data) {
|
|
508
|
+
if (!db.updateUser || !db.findUser) {
|
|
509
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
510
|
+
}
|
|
511
|
+
const user = await db.findUser(id);
|
|
512
|
+
if (!user) {
|
|
513
|
+
throw new UserNotFoundError(id);
|
|
514
|
+
}
|
|
515
|
+
if (data.email) {
|
|
516
|
+
if (!validateEmail(data.email)) {
|
|
517
|
+
throw new ValidationError("Invalid email format", { email: data.email });
|
|
518
|
+
}
|
|
519
|
+
data.email = data.email.toLowerCase();
|
|
520
|
+
}
|
|
521
|
+
return db.updateUser(id, data);
|
|
522
|
+
},
|
|
523
|
+
async deleteUser(id) {
|
|
524
|
+
if (!db.deleteUser || !db.findUser) {
|
|
525
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
526
|
+
}
|
|
527
|
+
const user = await db.findUser(id);
|
|
528
|
+
if (!user) {
|
|
529
|
+
throw new UserNotFoundError(id);
|
|
530
|
+
}
|
|
531
|
+
await db.deleteUser(id);
|
|
532
|
+
},
|
|
533
|
+
// ========================================
|
|
534
|
+
// Role Assignment
|
|
535
|
+
// ========================================
|
|
536
|
+
async assignRole(userId, roleId) {
|
|
537
|
+
if (!db.assignRoleToUser || !db.findUser || !db.findRole) {
|
|
538
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
539
|
+
}
|
|
540
|
+
const user = await db.findUser(userId);
|
|
541
|
+
if (!user) {
|
|
542
|
+
throw new UserNotFoundError(userId);
|
|
543
|
+
}
|
|
544
|
+
const role = await db.findRole(roleId);
|
|
545
|
+
if (!role) {
|
|
546
|
+
throw new RoleNotFoundError(roleId);
|
|
547
|
+
}
|
|
548
|
+
if (role.tenantId && user.tenantId && role.tenantId !== user.tenantId) {
|
|
549
|
+
throw new AuthorizationError("Role belongs to a different tenant");
|
|
550
|
+
}
|
|
551
|
+
await db.assignRoleToUser(userId, roleId);
|
|
552
|
+
},
|
|
553
|
+
async removeRole(userId, roleId) {
|
|
554
|
+
if (!db.removeRoleFromUser || !db.findUser) {
|
|
555
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
556
|
+
}
|
|
557
|
+
const user = await db.findUser(userId);
|
|
558
|
+
if (!user) {
|
|
559
|
+
throw new UserNotFoundError(userId);
|
|
560
|
+
}
|
|
561
|
+
await db.removeRoleFromUser(userId, roleId);
|
|
562
|
+
},
|
|
563
|
+
async getUserRoles(userId) {
|
|
564
|
+
if (!db.getUserRoles || !db.findUser) {
|
|
565
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
566
|
+
}
|
|
567
|
+
const user = await db.findUser(userId);
|
|
568
|
+
if (!user) {
|
|
569
|
+
throw new UserNotFoundError(userId);
|
|
570
|
+
}
|
|
571
|
+
return db.getUserRoles(userId);
|
|
572
|
+
},
|
|
573
|
+
// ========================================
|
|
574
|
+
// Permission Checking
|
|
575
|
+
// ========================================
|
|
576
|
+
async can(userId, action, resource, _resourceId) {
|
|
577
|
+
if (!db.findUser) {
|
|
578
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
579
|
+
}
|
|
580
|
+
const user = await db.findUser(userId);
|
|
581
|
+
if (!user) return false;
|
|
582
|
+
if (user.status !== "active") return false;
|
|
583
|
+
const permissions = await getAllUserPermissions(userId);
|
|
584
|
+
return hasPermission(permissions, action, resource);
|
|
585
|
+
},
|
|
586
|
+
async requirePermission(userId, action, resource, resourceId) {
|
|
587
|
+
const allowed = await this.can(userId, action, resource, resourceId);
|
|
588
|
+
if (!allowed) {
|
|
589
|
+
throw new AuthorizationError(
|
|
590
|
+
`Permission denied: ${action} on ${resource}${resourceId ? ` (${resourceId})` : ""}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
async canAny(userId, permissions) {
|
|
595
|
+
if (!db.findUser) {
|
|
596
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
597
|
+
}
|
|
598
|
+
const user = await db.findUser(userId);
|
|
599
|
+
if (!user) return false;
|
|
600
|
+
if (user.status !== "active") return false;
|
|
601
|
+
const userPermissions = await getAllUserPermissions(userId);
|
|
602
|
+
return permissions.some(
|
|
603
|
+
(required) => hasPermission(userPermissions, required.action, required.resource)
|
|
604
|
+
);
|
|
605
|
+
},
|
|
606
|
+
async isAdmin(userId) {
|
|
607
|
+
if (!db.findUser) {
|
|
608
|
+
throw new Error("Database adapter does not support RBAC operations");
|
|
609
|
+
}
|
|
610
|
+
const user = await db.findUser(userId);
|
|
611
|
+
if (!user || user.status !== "active") return false;
|
|
612
|
+
const permissions = await getAllUserPermissions(userId);
|
|
613
|
+
return isAdminPermission(permissions);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/audit.ts
|
|
619
|
+
var DEFAULT_RETENTION_DAYS = 90;
|
|
620
|
+
function auditLogToCsvRow(log) {
|
|
621
|
+
const fields = [
|
|
622
|
+
log.id,
|
|
623
|
+
log.tenantId || "",
|
|
624
|
+
log.userId || "",
|
|
625
|
+
log.action,
|
|
626
|
+
log.resource,
|
|
627
|
+
log.resourceId || "",
|
|
628
|
+
log.status,
|
|
629
|
+
log.ipAddress || "",
|
|
630
|
+
log.userAgent || "",
|
|
631
|
+
log.error || "",
|
|
632
|
+
log.createdAt.toISOString(),
|
|
633
|
+
JSON.stringify(log.details || {})
|
|
634
|
+
];
|
|
635
|
+
return fields.map((f) => `"${String(f).replace(/"/g, '""')}"`).join(",");
|
|
636
|
+
}
|
|
637
|
+
var CSV_HEADER = "id,tenant_id,user_id,action,resource,resource_id,status,ip_address,user_agent,error,created_at,details";
|
|
638
|
+
function createAuditManager(db, config) {
|
|
639
|
+
const retentionDays = config?.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
640
|
+
function shouldLog(action, resource) {
|
|
641
|
+
if (!config?.enabled) return false;
|
|
642
|
+
if (config.skipReadOperations && action === "read") {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
if (config.logActions && config.logActions.length > 0) {
|
|
646
|
+
if (!config.logActions.includes(action)) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (config.logResources && config.logResources.length > 0) {
|
|
651
|
+
if (!config.logResources.includes(resource)) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
async log(entry) {
|
|
659
|
+
if (!db.createAuditLog) {
|
|
660
|
+
throw new Error("Database adapter does not support audit operations");
|
|
661
|
+
}
|
|
662
|
+
if (config && !shouldLog(entry.action, entry.resource)) {
|
|
663
|
+
return {
|
|
664
|
+
id: "skipped",
|
|
665
|
+
...entry,
|
|
666
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
return db.createAuditLog(entry);
|
|
670
|
+
},
|
|
671
|
+
async list(filter) {
|
|
672
|
+
if (!db.listAuditLogs) {
|
|
673
|
+
throw new Error("Database adapter does not support audit operations");
|
|
674
|
+
}
|
|
675
|
+
return db.listAuditLogs(filter);
|
|
676
|
+
},
|
|
677
|
+
async getByResource(resource, resourceId) {
|
|
678
|
+
if (!db.listAuditLogs) {
|
|
679
|
+
throw new Error("Database adapter does not support audit operations");
|
|
680
|
+
}
|
|
681
|
+
const result = await db.listAuditLogs({
|
|
682
|
+
resource,
|
|
683
|
+
resourceId,
|
|
684
|
+
limit: 1e3
|
|
685
|
+
// Reasonable limit for resource-specific queries
|
|
686
|
+
});
|
|
687
|
+
return result.logs;
|
|
688
|
+
},
|
|
689
|
+
async getByUser(userId, filter) {
|
|
690
|
+
if (!db.listAuditLogs) {
|
|
691
|
+
throw new Error("Database adapter does not support audit operations");
|
|
692
|
+
}
|
|
693
|
+
return db.listAuditLogs({
|
|
694
|
+
...filter,
|
|
695
|
+
userId
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
async export(filter, format) {
|
|
699
|
+
if (!db.listAuditLogs) {
|
|
700
|
+
throw new Error("Database adapter does not support audit operations");
|
|
701
|
+
}
|
|
702
|
+
const allLogs = [];
|
|
703
|
+
let offset = 0;
|
|
704
|
+
const batchSize = 1e3;
|
|
705
|
+
while (true) {
|
|
706
|
+
const result = await db.listAuditLogs({
|
|
707
|
+
...filter,
|
|
708
|
+
limit: batchSize,
|
|
709
|
+
offset
|
|
710
|
+
});
|
|
711
|
+
allLogs.push(...result.logs);
|
|
712
|
+
if (result.logs.length < batchSize || allLogs.length >= 1e5) {
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
offset += batchSize;
|
|
716
|
+
}
|
|
717
|
+
if (format === "json") {
|
|
718
|
+
return JSON.stringify(allLogs, null, 2);
|
|
719
|
+
}
|
|
720
|
+
const rows = allLogs.map(auditLogToCsvRow);
|
|
721
|
+
return [CSV_HEADER, ...rows].join("\n");
|
|
722
|
+
},
|
|
723
|
+
async purge(olderThanDays) {
|
|
724
|
+
if (!db.deleteAuditLogs) {
|
|
725
|
+
throw new Error("Database adapter does not support audit operations");
|
|
726
|
+
}
|
|
727
|
+
const days = olderThanDays ?? retentionDays;
|
|
728
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
729
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
730
|
+
return db.deleteAuditLogs({ olderThan: cutoffDate });
|
|
731
|
+
},
|
|
732
|
+
async getSummary(tenantId, days = 30) {
|
|
733
|
+
if (!db.listAuditLogs) {
|
|
734
|
+
throw new Error("Database adapter does not support audit operations");
|
|
735
|
+
}
|
|
736
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
737
|
+
startDate.setDate(startDate.getDate() - days);
|
|
738
|
+
const result = await db.listAuditLogs({
|
|
739
|
+
tenantId,
|
|
740
|
+
startDate,
|
|
741
|
+
limit: 1e4
|
|
742
|
+
// Reasonable limit for summary
|
|
743
|
+
});
|
|
744
|
+
const logs = result.logs;
|
|
745
|
+
const byAction = {};
|
|
746
|
+
const byResource = {};
|
|
747
|
+
const byStatus = { success: 0, failure: 0 };
|
|
748
|
+
const userCounts = {};
|
|
749
|
+
const recentFailures = [];
|
|
750
|
+
for (const log of logs) {
|
|
751
|
+
byAction[log.action] = (byAction[log.action] || 0) + 1;
|
|
752
|
+
byResource[log.resource] = (byResource[log.resource] || 0) + 1;
|
|
753
|
+
byStatus[log.status]++;
|
|
754
|
+
if (log.userId) {
|
|
755
|
+
userCounts[log.userId] = (userCounts[log.userId] || 0) + 1;
|
|
756
|
+
}
|
|
757
|
+
if (log.status === "failure" && recentFailures.length < 10) {
|
|
758
|
+
recentFailures.push(log);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const topUsers = Object.entries(userCounts).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([userId, count]) => ({ userId, count }));
|
|
762
|
+
return {
|
|
763
|
+
totalLogs: result.total,
|
|
764
|
+
byAction,
|
|
765
|
+
byResource,
|
|
766
|
+
byStatus,
|
|
767
|
+
topUsers,
|
|
768
|
+
recentFailures
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/schedule.ts
|
|
775
|
+
function parseCronNextRun(cron, from = /* @__PURE__ */ new Date()) {
|
|
776
|
+
try {
|
|
777
|
+
const parts = cron.trim().split(/\s+/);
|
|
778
|
+
if (parts.length !== 5) return null;
|
|
779
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
780
|
+
const now = new Date(from);
|
|
781
|
+
const next = new Date(now);
|
|
782
|
+
next.setSeconds(0);
|
|
783
|
+
next.setMilliseconds(0);
|
|
784
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
785
|
+
const maxIterations = 365 * 24 * 60;
|
|
786
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
787
|
+
const matches = matchesCronField(minute, next.getMinutes()) && matchesCronField(hour, next.getHours()) && matchesCronField(dayOfMonth, next.getDate()) && matchesCronField(month, next.getMonth() + 1) && matchesCronField(dayOfWeek, next.getDay());
|
|
788
|
+
if (matches) {
|
|
789
|
+
return next;
|
|
790
|
+
}
|
|
791
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
} catch {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function matchesCronField(pattern, value) {
|
|
799
|
+
if (pattern === "*") return true;
|
|
800
|
+
if (pattern.startsWith("*/")) {
|
|
801
|
+
const step = parseInt(pattern.slice(2), 10);
|
|
802
|
+
return value % step === 0;
|
|
803
|
+
}
|
|
804
|
+
if (pattern.includes("-")) {
|
|
805
|
+
const [start, end] = pattern.split("-").map((n) => parseInt(n, 10));
|
|
806
|
+
return value >= start && value <= end;
|
|
807
|
+
}
|
|
808
|
+
if (pattern.includes(",")) {
|
|
809
|
+
const values = pattern.split(",").map((n) => parseInt(n, 10));
|
|
810
|
+
return values.includes(value);
|
|
811
|
+
}
|
|
812
|
+
return parseInt(pattern, 10) === value;
|
|
813
|
+
}
|
|
814
|
+
function calculateNextWindowRun(window, from = /* @__PURE__ */ new Date()) {
|
|
815
|
+
if (!window) return null;
|
|
816
|
+
const [startHour, startMin] = window.startTime.split(":").map(Number);
|
|
817
|
+
for (let dayOffset = 0; dayOffset <= 7; dayOffset++) {
|
|
818
|
+
const candidate = new Date(from);
|
|
819
|
+
candidate.setDate(candidate.getDate() + dayOffset);
|
|
820
|
+
candidate.setHours(startHour, startMin, 0, 0);
|
|
821
|
+
if (candidate <= from) continue;
|
|
822
|
+
if (window.daysOfWeek.includes(candidate.getDay())) {
|
|
823
|
+
return candidate;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
function createScheduleManager(db) {
|
|
829
|
+
function calculateNextRun(schedule) {
|
|
830
|
+
const now = /* @__PURE__ */ new Date();
|
|
831
|
+
switch (schedule.type) {
|
|
832
|
+
case "once":
|
|
833
|
+
if (schedule.executeAt && new Date(schedule.executeAt) > now) {
|
|
834
|
+
return new Date(schedule.executeAt);
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
case "recurring":
|
|
838
|
+
if (schedule.cron) {
|
|
839
|
+
return parseCronNextRun(schedule.cron, now);
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
case "window":
|
|
843
|
+
if (schedule.window) {
|
|
844
|
+
return calculateNextWindowRun(schedule.window, now);
|
|
845
|
+
}
|
|
846
|
+
return null;
|
|
847
|
+
default:
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
async get(id) {
|
|
853
|
+
if (!db.findScheduledTask) {
|
|
854
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
855
|
+
}
|
|
856
|
+
return db.findScheduledTask(id);
|
|
857
|
+
},
|
|
858
|
+
async list(filter) {
|
|
859
|
+
if (!db.listScheduledTasks) {
|
|
860
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
861
|
+
}
|
|
862
|
+
return db.listScheduledTasks(filter);
|
|
863
|
+
},
|
|
864
|
+
async create(data) {
|
|
865
|
+
if (!db.createScheduledTask) {
|
|
866
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
867
|
+
}
|
|
868
|
+
const nextRunAt = calculateNextRun(data.schedule);
|
|
869
|
+
const task = await db.createScheduledTask({
|
|
870
|
+
...data
|
|
871
|
+
// Note: nextRunAt is set by the database adapter based on schedule
|
|
872
|
+
});
|
|
873
|
+
if (nextRunAt && db.updateScheduledTask) {
|
|
874
|
+
return db.updateScheduledTask(task.id, {
|
|
875
|
+
...data
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
return task;
|
|
879
|
+
},
|
|
880
|
+
async update(id, data) {
|
|
881
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
882
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
883
|
+
}
|
|
884
|
+
const existing = await db.findScheduledTask(id);
|
|
885
|
+
if (!existing) {
|
|
886
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
887
|
+
}
|
|
888
|
+
return db.updateScheduledTask(id, data);
|
|
889
|
+
},
|
|
890
|
+
async delete(id) {
|
|
891
|
+
if (!db.deleteScheduledTask) {
|
|
892
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
893
|
+
}
|
|
894
|
+
await db.deleteScheduledTask(id);
|
|
895
|
+
},
|
|
896
|
+
async pause(id) {
|
|
897
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
898
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
899
|
+
}
|
|
900
|
+
const task = await db.findScheduledTask(id);
|
|
901
|
+
if (!task) {
|
|
902
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
903
|
+
}
|
|
904
|
+
if (task.status === "paused") {
|
|
905
|
+
return task;
|
|
906
|
+
}
|
|
907
|
+
return db.updateScheduledTask(id, { status: "paused" });
|
|
908
|
+
},
|
|
909
|
+
async resume(id) {
|
|
910
|
+
if (!db.updateScheduledTask || !db.findScheduledTask) {
|
|
911
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
912
|
+
}
|
|
913
|
+
const task = await db.findScheduledTask(id);
|
|
914
|
+
if (!task) {
|
|
915
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
916
|
+
}
|
|
917
|
+
if (task.status !== "paused") {
|
|
918
|
+
return task;
|
|
919
|
+
}
|
|
920
|
+
calculateNextRun(task.schedule);
|
|
921
|
+
return db.updateScheduledTask(id, { status: "active" });
|
|
922
|
+
},
|
|
923
|
+
async runNow(id) {
|
|
924
|
+
if (!db.findScheduledTask || !db.createTaskExecution || !db.updateScheduledTask) {
|
|
925
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
926
|
+
}
|
|
927
|
+
const task = await db.findScheduledTask(id);
|
|
928
|
+
if (!task) {
|
|
929
|
+
throw new Error(`Scheduled task not found: ${id}`);
|
|
930
|
+
}
|
|
931
|
+
const execution = await db.createTaskExecution({ taskId: id });
|
|
932
|
+
await db.updateScheduledTask(id, {});
|
|
933
|
+
return execution;
|
|
934
|
+
},
|
|
935
|
+
async getUpcoming(hours) {
|
|
936
|
+
if (!db.getUpcomingTasks) {
|
|
937
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
938
|
+
}
|
|
939
|
+
return db.getUpcomingTasks(hours);
|
|
940
|
+
},
|
|
941
|
+
async getExecutions(taskId, limit = 10) {
|
|
942
|
+
if (!db.listTaskExecutions) {
|
|
943
|
+
throw new Error("Database adapter does not support task scheduling");
|
|
944
|
+
}
|
|
945
|
+
return db.listTaskExecutions(taskId, limit);
|
|
946
|
+
},
|
|
947
|
+
calculateNextRun
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/queue.ts
|
|
952
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
953
|
+
var DEFAULT_TTL_SECONDS = 86400;
|
|
954
|
+
function createMessageQueueManager(db) {
|
|
955
|
+
return {
|
|
956
|
+
async enqueue(message) {
|
|
957
|
+
if (!db.enqueueMessage) {
|
|
958
|
+
throw new Error("Database adapter does not support message queue");
|
|
959
|
+
}
|
|
960
|
+
const enrichedMessage = {
|
|
961
|
+
...message,
|
|
962
|
+
priority: message.priority ?? "normal",
|
|
963
|
+
maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
964
|
+
ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS
|
|
965
|
+
};
|
|
966
|
+
return db.enqueueMessage(enrichedMessage);
|
|
967
|
+
},
|
|
968
|
+
async enqueueBatch(messages) {
|
|
969
|
+
if (!db.enqueueMessage) {
|
|
970
|
+
throw new Error("Database adapter does not support message queue");
|
|
971
|
+
}
|
|
972
|
+
const results = [];
|
|
973
|
+
for (const message of messages) {
|
|
974
|
+
const enrichedMessage = {
|
|
975
|
+
...message,
|
|
976
|
+
priority: message.priority ?? "normal",
|
|
977
|
+
maxAttempts: message.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
978
|
+
ttlSeconds: message.ttlSeconds ?? DEFAULT_TTL_SECONDS
|
|
979
|
+
};
|
|
980
|
+
const queued = await db.enqueueMessage(enrichedMessage);
|
|
981
|
+
results.push(queued);
|
|
982
|
+
}
|
|
983
|
+
return results;
|
|
984
|
+
},
|
|
985
|
+
async dequeue(deviceId, limit = 10) {
|
|
986
|
+
if (!db.dequeueMessages) {
|
|
987
|
+
throw new Error("Database adapter does not support message queue");
|
|
988
|
+
}
|
|
989
|
+
return db.dequeueMessages(deviceId, limit);
|
|
990
|
+
},
|
|
991
|
+
async acknowledge(messageId) {
|
|
992
|
+
if (!db.acknowledgeMessage) {
|
|
993
|
+
throw new Error("Database adapter does not support message queue");
|
|
994
|
+
}
|
|
995
|
+
await db.acknowledgeMessage(messageId);
|
|
996
|
+
},
|
|
997
|
+
async fail(messageId, error) {
|
|
998
|
+
if (!db.failMessage) {
|
|
999
|
+
throw new Error("Database adapter does not support message queue");
|
|
1000
|
+
}
|
|
1001
|
+
await db.failMessage(messageId, error);
|
|
1002
|
+
},
|
|
1003
|
+
async retryFailed(maxAttempts = DEFAULT_MAX_ATTEMPTS) {
|
|
1004
|
+
if (!db.retryFailedMessages) {
|
|
1005
|
+
throw new Error("Database adapter does not support message queue");
|
|
1006
|
+
}
|
|
1007
|
+
return db.retryFailedMessages(maxAttempts);
|
|
1008
|
+
},
|
|
1009
|
+
async purgeExpired() {
|
|
1010
|
+
if (!db.purgeExpiredMessages) {
|
|
1011
|
+
throw new Error("Database adapter does not support message queue");
|
|
1012
|
+
}
|
|
1013
|
+
return db.purgeExpiredMessages();
|
|
1014
|
+
},
|
|
1015
|
+
async getStats(tenantId) {
|
|
1016
|
+
if (!db.getQueueStats) {
|
|
1017
|
+
throw new Error("Database adapter does not support message queue");
|
|
1018
|
+
}
|
|
1019
|
+
return db.getQueueStats(tenantId);
|
|
1020
|
+
},
|
|
1021
|
+
async peek(deviceId, limit = 10) {
|
|
1022
|
+
if (!db.peekMessages) {
|
|
1023
|
+
throw new Error("Database adapter does not support message queue");
|
|
1024
|
+
}
|
|
1025
|
+
return db.peekMessages(deviceId, limit);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/dashboard.ts
|
|
1031
|
+
function createDashboardManager(db) {
|
|
1032
|
+
return {
|
|
1033
|
+
async getStats(_tenantId) {
|
|
1034
|
+
if (db.getDashboardStats) {
|
|
1035
|
+
return db.getDashboardStats(_tenantId);
|
|
1036
|
+
}
|
|
1037
|
+
const devices = await db.listDevices({
|
|
1038
|
+
limit: 1e4
|
|
1039
|
+
// Get all for counting
|
|
1040
|
+
});
|
|
1041
|
+
const deviceStats = {
|
|
1042
|
+
total: devices.total,
|
|
1043
|
+
enrolled: devices.devices.filter((d) => d.status === "enrolled").length,
|
|
1044
|
+
active: devices.devices.filter((d) => d.status === "enrolled").length,
|
|
1045
|
+
// 'active' = 'enrolled' for dashboard
|
|
1046
|
+
blocked: devices.devices.filter((d) => d.status === "blocked").length,
|
|
1047
|
+
pending: devices.devices.filter((d) => d.status === "pending").length
|
|
1048
|
+
};
|
|
1049
|
+
const allPolicies = await db.listPolicies();
|
|
1050
|
+
const policyStats = {
|
|
1051
|
+
total: allPolicies.length,
|
|
1052
|
+
deployed: allPolicies.filter((p) => p.isDefault).length
|
|
1053
|
+
};
|
|
1054
|
+
const allApps = await db.listApplications();
|
|
1055
|
+
const appStats = {
|
|
1056
|
+
total: allApps.length,
|
|
1057
|
+
deployed: allApps.length
|
|
1058
|
+
// All apps in db are considered deployed
|
|
1059
|
+
};
|
|
1060
|
+
const now = /* @__PURE__ */ new Date();
|
|
1061
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
1062
|
+
const allCommands = await db.listCommands({ limit: 1e4 });
|
|
1063
|
+
const pendingCommands = allCommands.filter((c) => c.status === "pending");
|
|
1064
|
+
const last24hCommands = allCommands.filter(
|
|
1065
|
+
(c) => new Date(c.createdAt) >= yesterday
|
|
1066
|
+
);
|
|
1067
|
+
const commandStats = {
|
|
1068
|
+
pendingCount: pendingCommands.length,
|
|
1069
|
+
last24hTotal: last24hCommands.length,
|
|
1070
|
+
last24hSuccess: last24hCommands.filter((c) => c.status === "completed").length,
|
|
1071
|
+
last24hFailed: last24hCommands.filter((c) => c.status === "failed").length
|
|
1072
|
+
};
|
|
1073
|
+
const allGroups = await db.listGroups();
|
|
1074
|
+
let groupsWithDevices = 0;
|
|
1075
|
+
for (const group of allGroups) {
|
|
1076
|
+
const groupDevices = await db.listDevicesInGroup(group.id);
|
|
1077
|
+
if (groupDevices.length > 0) groupsWithDevices++;
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
devices: deviceStats,
|
|
1081
|
+
policies: policyStats,
|
|
1082
|
+
applications: appStats,
|
|
1083
|
+
commands: commandStats,
|
|
1084
|
+
groups: {
|
|
1085
|
+
total: allGroups.length,
|
|
1086
|
+
withDevices: groupsWithDevices
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
},
|
|
1090
|
+
async getDeviceStatusBreakdown(_tenantId) {
|
|
1091
|
+
if (db.getDeviceStatusBreakdown) {
|
|
1092
|
+
return db.getDeviceStatusBreakdown(_tenantId);
|
|
1093
|
+
}
|
|
1094
|
+
const devices = await db.listDevices({
|
|
1095
|
+
limit: 1e4
|
|
1096
|
+
});
|
|
1097
|
+
const byStatus = {
|
|
1098
|
+
pending: 0,
|
|
1099
|
+
enrolled: 0,
|
|
1100
|
+
blocked: 0,
|
|
1101
|
+
unenrolled: 0
|
|
1102
|
+
};
|
|
1103
|
+
const byOs = {};
|
|
1104
|
+
const byManufacturer = {};
|
|
1105
|
+
const byModel = {};
|
|
1106
|
+
for (const device of devices.devices) {
|
|
1107
|
+
byStatus[device.status]++;
|
|
1108
|
+
const osKey = device.osVersion || "Unknown";
|
|
1109
|
+
byOs[osKey] = (byOs[osKey] || 0) + 1;
|
|
1110
|
+
const mfr = device.manufacturer || "Unknown";
|
|
1111
|
+
byManufacturer[mfr] = (byManufacturer[mfr] || 0) + 1;
|
|
1112
|
+
const model = device.model || "Unknown";
|
|
1113
|
+
byModel[model] = (byModel[model] || 0) + 1;
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
byStatus,
|
|
1117
|
+
byOs,
|
|
1118
|
+
byManufacturer,
|
|
1119
|
+
byModel
|
|
1120
|
+
};
|
|
1121
|
+
},
|
|
1122
|
+
async getEnrollmentTrend(days, _tenantId) {
|
|
1123
|
+
if (db.getEnrollmentTrend) {
|
|
1124
|
+
return db.getEnrollmentTrend(days, _tenantId);
|
|
1125
|
+
}
|
|
1126
|
+
const now = /* @__PURE__ */ new Date();
|
|
1127
|
+
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1128
|
+
const events = await db.listEvents({
|
|
1129
|
+
type: "device.enrolled",
|
|
1130
|
+
startDate,
|
|
1131
|
+
limit: 1e4
|
|
1132
|
+
});
|
|
1133
|
+
const unenrollEvents = await db.listEvents({
|
|
1134
|
+
type: "device.unenrolled",
|
|
1135
|
+
startDate,
|
|
1136
|
+
limit: 1e4
|
|
1137
|
+
});
|
|
1138
|
+
const trendByDate = /* @__PURE__ */ new Map();
|
|
1139
|
+
for (let i = 0; i < days; i++) {
|
|
1140
|
+
const date = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1e3);
|
|
1141
|
+
const dateKey = date.toISOString().split("T")[0];
|
|
1142
|
+
trendByDate.set(dateKey, { enrolled: 0, unenrolled: 0 });
|
|
1143
|
+
}
|
|
1144
|
+
for (const event of events) {
|
|
1145
|
+
const dateKey = new Date(event.createdAt).toISOString().split("T")[0];
|
|
1146
|
+
const entry = trendByDate.get(dateKey);
|
|
1147
|
+
if (entry) {
|
|
1148
|
+
entry.enrolled++;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
for (const event of unenrollEvents) {
|
|
1152
|
+
const dateKey = new Date(event.createdAt).toISOString().split("T")[0];
|
|
1153
|
+
const entry = trendByDate.get(dateKey);
|
|
1154
|
+
if (entry) {
|
|
1155
|
+
entry.unenrolled++;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const initialDevices = await db.listDevices({
|
|
1159
|
+
limit: 1e4
|
|
1160
|
+
});
|
|
1161
|
+
let runningTotal = initialDevices.total;
|
|
1162
|
+
const result = [];
|
|
1163
|
+
const sortedDates = Array.from(trendByDate.keys()).sort();
|
|
1164
|
+
for (const dateKey of sortedDates) {
|
|
1165
|
+
const entry = trendByDate.get(dateKey);
|
|
1166
|
+
const netChange = entry.enrolled - entry.unenrolled;
|
|
1167
|
+
runningTotal += netChange;
|
|
1168
|
+
result.push({
|
|
1169
|
+
date: new Date(dateKey),
|
|
1170
|
+
enrolled: entry.enrolled,
|
|
1171
|
+
unenrolled: entry.unenrolled,
|
|
1172
|
+
netChange,
|
|
1173
|
+
totalDevices: runningTotal
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return result;
|
|
1177
|
+
},
|
|
1178
|
+
async getCommandSuccessRates(_tenantId) {
|
|
1179
|
+
if (db.getCommandSuccessRates) {
|
|
1180
|
+
return db.getCommandSuccessRates(_tenantId);
|
|
1181
|
+
}
|
|
1182
|
+
const now = /* @__PURE__ */ new Date();
|
|
1183
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
1184
|
+
const commands = await db.listCommands({ limit: 1e4 });
|
|
1185
|
+
const completed = commands.filter((c) => c.status === "completed").length;
|
|
1186
|
+
const failed = commands.filter((c) => c.status === "failed").length;
|
|
1187
|
+
const total = commands.length;
|
|
1188
|
+
const byType = {};
|
|
1189
|
+
for (const cmd of commands) {
|
|
1190
|
+
if (!byType[cmd.type]) {
|
|
1191
|
+
byType[cmd.type] = {
|
|
1192
|
+
total: 0,
|
|
1193
|
+
completed: 0,
|
|
1194
|
+
failed: 0,
|
|
1195
|
+
successRate: 0
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
byType[cmd.type].total++;
|
|
1199
|
+
if (cmd.status === "completed") byType[cmd.type].completed++;
|
|
1200
|
+
if (cmd.status === "failed") byType[cmd.type].failed++;
|
|
1201
|
+
}
|
|
1202
|
+
for (const type of Object.keys(byType)) {
|
|
1203
|
+
const stats = byType[type];
|
|
1204
|
+
const finishedCount = stats.completed + stats.failed;
|
|
1205
|
+
stats.successRate = finishedCount > 0 ? stats.completed / finishedCount * 100 : 0;
|
|
1206
|
+
}
|
|
1207
|
+
const last24hCommands = commands.filter(
|
|
1208
|
+
(c) => new Date(c.createdAt) >= yesterday
|
|
1209
|
+
);
|
|
1210
|
+
return {
|
|
1211
|
+
overall: {
|
|
1212
|
+
total,
|
|
1213
|
+
completed,
|
|
1214
|
+
failed,
|
|
1215
|
+
successRate: completed + failed > 0 ? completed / (completed + failed) * 100 : 0
|
|
1216
|
+
},
|
|
1217
|
+
byType,
|
|
1218
|
+
last24h: {
|
|
1219
|
+
total: last24hCommands.length,
|
|
1220
|
+
completed: last24hCommands.filter((c) => c.status === "completed").length,
|
|
1221
|
+
failed: last24hCommands.filter((c) => c.status === "failed").length,
|
|
1222
|
+
pending: last24hCommands.filter((c) => c.status === "pending").length
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
166
1225
|
},
|
|
167
|
-
|
|
168
|
-
|
|
1226
|
+
async getAppInstallationSummary(_tenantId) {
|
|
1227
|
+
if (db.getAppInstallationSummary) {
|
|
1228
|
+
return db.getAppInstallationSummary(_tenantId);
|
|
1229
|
+
}
|
|
1230
|
+
const apps = await db.listApplications();
|
|
1231
|
+
const appMap = new Map(apps.map((a) => [a.packageName, a]));
|
|
1232
|
+
const byStatus = {
|
|
1233
|
+
installed: 0,
|
|
1234
|
+
installing: 0,
|
|
1235
|
+
failed: 0,
|
|
1236
|
+
pending: 0
|
|
1237
|
+
};
|
|
1238
|
+
const installCounts = {};
|
|
1239
|
+
const devices = await db.listDevices({
|
|
1240
|
+
limit: 1e4
|
|
1241
|
+
});
|
|
1242
|
+
for (const device of devices.devices) {
|
|
1243
|
+
if (device.installedApps) {
|
|
1244
|
+
for (const app of device.installedApps) {
|
|
1245
|
+
const key = app.packageName;
|
|
1246
|
+
installCounts[key] = (installCounts[key] || 0) + 1;
|
|
1247
|
+
byStatus["installed"]++;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const topInstalled = Object.entries(installCounts).sort(([, a], [, b]) => b - a).slice(0, 10).map(([packageName, count]) => ({
|
|
1252
|
+
packageName,
|
|
1253
|
+
name: appMap.get(packageName)?.name || packageName,
|
|
1254
|
+
installedCount: count
|
|
1255
|
+
}));
|
|
1256
|
+
return {
|
|
1257
|
+
total: Object.values(byStatus).reduce((a, b) => a + b, 0),
|
|
1258
|
+
byStatus,
|
|
1259
|
+
recentFailures: [],
|
|
1260
|
+
// Would need installation status tracking
|
|
1261
|
+
topInstalled
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/plugin-storage.ts
|
|
1268
|
+
function createPluginStorageAdapter(db) {
|
|
1269
|
+
return {
|
|
1270
|
+
async get(pluginName, key) {
|
|
1271
|
+
if (db.getPluginValue) {
|
|
1272
|
+
const value = await db.getPluginValue(pluginName, key);
|
|
1273
|
+
return value;
|
|
1274
|
+
}
|
|
1275
|
+
console.warn("Plugin storage not supported by database adapter");
|
|
1276
|
+
return null;
|
|
169
1277
|
},
|
|
170
|
-
|
|
171
|
-
|
|
1278
|
+
async set(pluginName, key, value) {
|
|
1279
|
+
if (db.setPluginValue) {
|
|
1280
|
+
await db.setPluginValue(pluginName, key, value);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
console.warn("Plugin storage not supported by database adapter");
|
|
172
1284
|
},
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
1285
|
+
async delete(pluginName, key) {
|
|
1286
|
+
if (db.deletePluginValue) {
|
|
1287
|
+
await db.deletePluginValue(pluginName, key);
|
|
1288
|
+
return;
|
|
177
1289
|
}
|
|
1290
|
+
console.warn("Plugin storage not supported by database adapter");
|
|
178
1291
|
},
|
|
179
|
-
|
|
180
|
-
|
|
1292
|
+
async list(pluginName, prefix) {
|
|
1293
|
+
if (db.listPluginKeys) {
|
|
1294
|
+
return db.listPluginKeys(pluginName, prefix);
|
|
1295
|
+
}
|
|
1296
|
+
console.warn("Plugin storage not supported by database adapter");
|
|
1297
|
+
return [];
|
|
181
1298
|
},
|
|
182
|
-
async
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
endpointId,
|
|
187
|
-
success: false,
|
|
188
|
-
error: "Endpoint not found",
|
|
189
|
-
retryCount: 0
|
|
190
|
-
};
|
|
1299
|
+
async clear(pluginName) {
|
|
1300
|
+
if (db.clearPluginData) {
|
|
1301
|
+
await db.clearPluginData(pluginName);
|
|
1302
|
+
return;
|
|
191
1303
|
}
|
|
192
|
-
|
|
193
|
-
id: randomUUID(),
|
|
194
|
-
event: "device.heartbeat",
|
|
195
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
196
|
-
data: {
|
|
197
|
-
test: true,
|
|
198
|
-
message: "OpenMDM webhook test"
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
return deliverToEndpoint(endpoint, testPayload);
|
|
1304
|
+
console.warn("Plugin storage not supported by database adapter");
|
|
202
1305
|
}
|
|
203
1306
|
};
|
|
204
1307
|
}
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
|
|
1308
|
+
function createMemoryPluginStorageAdapter() {
|
|
1309
|
+
const store = /* @__PURE__ */ new Map();
|
|
1310
|
+
function getPluginStore(pluginName) {
|
|
1311
|
+
if (!store.has(pluginName)) {
|
|
1312
|
+
store.set(pluginName, /* @__PURE__ */ new Map());
|
|
1313
|
+
}
|
|
1314
|
+
return store.get(pluginName);
|
|
213
1315
|
}
|
|
214
|
-
return
|
|
1316
|
+
return {
|
|
1317
|
+
async get(pluginName, key) {
|
|
1318
|
+
const pluginStore = getPluginStore(pluginName);
|
|
1319
|
+
const value = pluginStore.get(key);
|
|
1320
|
+
return value === void 0 ? null : value;
|
|
1321
|
+
},
|
|
1322
|
+
async set(pluginName, key, value) {
|
|
1323
|
+
const pluginStore = getPluginStore(pluginName);
|
|
1324
|
+
pluginStore.set(key, value);
|
|
1325
|
+
},
|
|
1326
|
+
async delete(pluginName, key) {
|
|
1327
|
+
const pluginStore = getPluginStore(pluginName);
|
|
1328
|
+
pluginStore.delete(key);
|
|
1329
|
+
},
|
|
1330
|
+
async list(pluginName, prefix) {
|
|
1331
|
+
const pluginStore = getPluginStore(pluginName);
|
|
1332
|
+
const keys = Array.from(pluginStore.keys());
|
|
1333
|
+
if (prefix) {
|
|
1334
|
+
return keys.filter((k) => k.startsWith(prefix));
|
|
1335
|
+
}
|
|
1336
|
+
return keys;
|
|
1337
|
+
},
|
|
1338
|
+
async clear(pluginName) {
|
|
1339
|
+
store.delete(pluginName);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function createPluginKey(namespace, ...parts) {
|
|
1344
|
+
return [namespace, ...parts].join(":");
|
|
1345
|
+
}
|
|
1346
|
+
function parsePluginKey(key) {
|
|
1347
|
+
const [namespace, ...parts] = key.split(":");
|
|
1348
|
+
return { namespace, parts };
|
|
215
1349
|
}
|
|
216
1350
|
|
|
217
1351
|
// src/schema.ts
|
|
@@ -586,6 +1720,265 @@ var mdmSchema = {
|
|
|
586
1720
|
{ columns: ["status"] },
|
|
587
1721
|
{ columns: ["created_at"] }
|
|
588
1722
|
]
|
|
1723
|
+
},
|
|
1724
|
+
// ----------------------------------------
|
|
1725
|
+
// Tenants Table (Multi-tenancy)
|
|
1726
|
+
// ----------------------------------------
|
|
1727
|
+
mdm_tenants: {
|
|
1728
|
+
columns: {
|
|
1729
|
+
id: { type: "string", primaryKey: true },
|
|
1730
|
+
name: { type: "string" },
|
|
1731
|
+
slug: { type: "string", unique: true },
|
|
1732
|
+
status: {
|
|
1733
|
+
type: "enum",
|
|
1734
|
+
enumValues: ["active", "suspended", "pending"],
|
|
1735
|
+
default: "pending"
|
|
1736
|
+
},
|
|
1737
|
+
settings: { type: "json", nullable: true },
|
|
1738
|
+
metadata: { type: "json", nullable: true },
|
|
1739
|
+
created_at: { type: "datetime", default: "now" },
|
|
1740
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1741
|
+
},
|
|
1742
|
+
indexes: [
|
|
1743
|
+
{ columns: ["slug"], unique: true },
|
|
1744
|
+
{ columns: ["status"] }
|
|
1745
|
+
]
|
|
1746
|
+
},
|
|
1747
|
+
// ----------------------------------------
|
|
1748
|
+
// Roles Table (RBAC)
|
|
1749
|
+
// ----------------------------------------
|
|
1750
|
+
mdm_roles: {
|
|
1751
|
+
columns: {
|
|
1752
|
+
id: { type: "string", primaryKey: true },
|
|
1753
|
+
tenant_id: {
|
|
1754
|
+
type: "string",
|
|
1755
|
+
nullable: true,
|
|
1756
|
+
references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
|
|
1757
|
+
},
|
|
1758
|
+
name: { type: "string" },
|
|
1759
|
+
description: { type: "text", nullable: true },
|
|
1760
|
+
permissions: { type: "json" },
|
|
1761
|
+
is_system: { type: "boolean", default: false },
|
|
1762
|
+
created_at: { type: "datetime", default: "now" },
|
|
1763
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1764
|
+
},
|
|
1765
|
+
indexes: [
|
|
1766
|
+
{ columns: ["tenant_id"] },
|
|
1767
|
+
{ columns: ["name"] },
|
|
1768
|
+
{ columns: ["tenant_id", "name"], unique: true }
|
|
1769
|
+
]
|
|
1770
|
+
},
|
|
1771
|
+
// ----------------------------------------
|
|
1772
|
+
// Users Table (RBAC)
|
|
1773
|
+
// ----------------------------------------
|
|
1774
|
+
mdm_users: {
|
|
1775
|
+
columns: {
|
|
1776
|
+
id: { type: "string", primaryKey: true },
|
|
1777
|
+
tenant_id: {
|
|
1778
|
+
type: "string",
|
|
1779
|
+
nullable: true,
|
|
1780
|
+
references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
|
|
1781
|
+
},
|
|
1782
|
+
email: { type: "string" },
|
|
1783
|
+
name: { type: "string", nullable: true },
|
|
1784
|
+
status: {
|
|
1785
|
+
type: "enum",
|
|
1786
|
+
enumValues: ["active", "inactive", "pending"],
|
|
1787
|
+
default: "pending"
|
|
1788
|
+
},
|
|
1789
|
+
metadata: { type: "json", nullable: true },
|
|
1790
|
+
last_login_at: { type: "datetime", nullable: true },
|
|
1791
|
+
created_at: { type: "datetime", default: "now" },
|
|
1792
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1793
|
+
},
|
|
1794
|
+
indexes: [
|
|
1795
|
+
{ columns: ["tenant_id"] },
|
|
1796
|
+
{ columns: ["email"] },
|
|
1797
|
+
{ columns: ["tenant_id", "email"], unique: true },
|
|
1798
|
+
{ columns: ["status"] }
|
|
1799
|
+
]
|
|
1800
|
+
},
|
|
1801
|
+
// ----------------------------------------
|
|
1802
|
+
// User Roles (Many-to-Many)
|
|
1803
|
+
// ----------------------------------------
|
|
1804
|
+
mdm_user_roles: {
|
|
1805
|
+
columns: {
|
|
1806
|
+
user_id: {
|
|
1807
|
+
type: "string",
|
|
1808
|
+
references: { table: "mdm_users", column: "id", onDelete: "cascade" }
|
|
1809
|
+
},
|
|
1810
|
+
role_id: {
|
|
1811
|
+
type: "string",
|
|
1812
|
+
references: { table: "mdm_roles", column: "id", onDelete: "cascade" }
|
|
1813
|
+
},
|
|
1814
|
+
created_at: { type: "datetime", default: "now" }
|
|
1815
|
+
},
|
|
1816
|
+
indexes: [
|
|
1817
|
+
{ columns: ["user_id", "role_id"], unique: true },
|
|
1818
|
+
{ columns: ["user_id"] },
|
|
1819
|
+
{ columns: ["role_id"] }
|
|
1820
|
+
]
|
|
1821
|
+
},
|
|
1822
|
+
// ----------------------------------------
|
|
1823
|
+
// Audit Logs Table
|
|
1824
|
+
// ----------------------------------------
|
|
1825
|
+
mdm_audit_logs: {
|
|
1826
|
+
columns: {
|
|
1827
|
+
id: { type: "string", primaryKey: true },
|
|
1828
|
+
tenant_id: {
|
|
1829
|
+
type: "string",
|
|
1830
|
+
nullable: true,
|
|
1831
|
+
references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
|
|
1832
|
+
},
|
|
1833
|
+
user_id: {
|
|
1834
|
+
type: "string",
|
|
1835
|
+
nullable: true,
|
|
1836
|
+
references: { table: "mdm_users", column: "id", onDelete: "set null" }
|
|
1837
|
+
},
|
|
1838
|
+
action: { type: "string" },
|
|
1839
|
+
resource: { type: "string" },
|
|
1840
|
+
resource_id: { type: "string", nullable: true },
|
|
1841
|
+
details: { type: "json", nullable: true },
|
|
1842
|
+
ip_address: { type: "string", nullable: true },
|
|
1843
|
+
user_agent: { type: "text", nullable: true },
|
|
1844
|
+
created_at: { type: "datetime", default: "now" }
|
|
1845
|
+
},
|
|
1846
|
+
indexes: [
|
|
1847
|
+
{ columns: ["tenant_id"] },
|
|
1848
|
+
{ columns: ["user_id"] },
|
|
1849
|
+
{ columns: ["action"] },
|
|
1850
|
+
{ columns: ["resource"] },
|
|
1851
|
+
{ columns: ["resource", "resource_id"] },
|
|
1852
|
+
{ columns: ["created_at"] }
|
|
1853
|
+
]
|
|
1854
|
+
},
|
|
1855
|
+
// ----------------------------------------
|
|
1856
|
+
// Scheduled Tasks Table
|
|
1857
|
+
// ----------------------------------------
|
|
1858
|
+
mdm_scheduled_tasks: {
|
|
1859
|
+
columns: {
|
|
1860
|
+
id: { type: "string", primaryKey: true },
|
|
1861
|
+
tenant_id: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
nullable: true,
|
|
1864
|
+
references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
|
|
1865
|
+
},
|
|
1866
|
+
name: { type: "string" },
|
|
1867
|
+
description: { type: "text", nullable: true },
|
|
1868
|
+
task_type: {
|
|
1869
|
+
type: "enum",
|
|
1870
|
+
enumValues: ["command", "policy_update", "app_install", "maintenance", "custom"]
|
|
1871
|
+
},
|
|
1872
|
+
schedule: { type: "json" },
|
|
1873
|
+
target: { type: "json", nullable: true },
|
|
1874
|
+
payload: { type: "json", nullable: true },
|
|
1875
|
+
status: {
|
|
1876
|
+
type: "enum",
|
|
1877
|
+
enumValues: ["active", "paused", "completed", "failed"],
|
|
1878
|
+
default: "active"
|
|
1879
|
+
},
|
|
1880
|
+
next_run_at: { type: "datetime", nullable: true },
|
|
1881
|
+
last_run_at: { type: "datetime", nullable: true },
|
|
1882
|
+
max_retries: { type: "integer", default: 3 },
|
|
1883
|
+
retry_count: { type: "integer", default: 0 },
|
|
1884
|
+
created_at: { type: "datetime", default: "now" },
|
|
1885
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1886
|
+
},
|
|
1887
|
+
indexes: [
|
|
1888
|
+
{ columns: ["tenant_id"] },
|
|
1889
|
+
{ columns: ["task_type"] },
|
|
1890
|
+
{ columns: ["status"] },
|
|
1891
|
+
{ columns: ["next_run_at"] }
|
|
1892
|
+
]
|
|
1893
|
+
},
|
|
1894
|
+
// ----------------------------------------
|
|
1895
|
+
// Task Executions Table
|
|
1896
|
+
// ----------------------------------------
|
|
1897
|
+
mdm_task_executions: {
|
|
1898
|
+
columns: {
|
|
1899
|
+
id: { type: "string", primaryKey: true },
|
|
1900
|
+
task_id: {
|
|
1901
|
+
type: "string",
|
|
1902
|
+
references: { table: "mdm_scheduled_tasks", column: "id", onDelete: "cascade" }
|
|
1903
|
+
},
|
|
1904
|
+
status: {
|
|
1905
|
+
type: "enum",
|
|
1906
|
+
enumValues: ["running", "completed", "failed"],
|
|
1907
|
+
default: "running"
|
|
1908
|
+
},
|
|
1909
|
+
started_at: { type: "datetime", default: "now" },
|
|
1910
|
+
completed_at: { type: "datetime", nullable: true },
|
|
1911
|
+
devices_processed: { type: "integer", default: 0 },
|
|
1912
|
+
devices_succeeded: { type: "integer", default: 0 },
|
|
1913
|
+
devices_failed: { type: "integer", default: 0 },
|
|
1914
|
+
error: { type: "text", nullable: true },
|
|
1915
|
+
details: { type: "json", nullable: true }
|
|
1916
|
+
},
|
|
1917
|
+
indexes: [
|
|
1918
|
+
{ columns: ["task_id"] },
|
|
1919
|
+
{ columns: ["status"] },
|
|
1920
|
+
{ columns: ["started_at"] }
|
|
1921
|
+
]
|
|
1922
|
+
},
|
|
1923
|
+
// ----------------------------------------
|
|
1924
|
+
// Message Queue Table
|
|
1925
|
+
// ----------------------------------------
|
|
1926
|
+
mdm_message_queue: {
|
|
1927
|
+
columns: {
|
|
1928
|
+
id: { type: "string", primaryKey: true },
|
|
1929
|
+
tenant_id: {
|
|
1930
|
+
type: "string",
|
|
1931
|
+
nullable: true,
|
|
1932
|
+
references: { table: "mdm_tenants", column: "id", onDelete: "cascade" }
|
|
1933
|
+
},
|
|
1934
|
+
device_id: {
|
|
1935
|
+
type: "string",
|
|
1936
|
+
references: { table: "mdm_devices", column: "id", onDelete: "cascade" }
|
|
1937
|
+
},
|
|
1938
|
+
message_type: { type: "string" },
|
|
1939
|
+
payload: { type: "json" },
|
|
1940
|
+
priority: {
|
|
1941
|
+
type: "enum",
|
|
1942
|
+
enumValues: ["high", "normal", "low"],
|
|
1943
|
+
default: "normal"
|
|
1944
|
+
},
|
|
1945
|
+
status: {
|
|
1946
|
+
type: "enum",
|
|
1947
|
+
enumValues: ["pending", "processing", "delivered", "failed", "expired"],
|
|
1948
|
+
default: "pending"
|
|
1949
|
+
},
|
|
1950
|
+
attempts: { type: "integer", default: 0 },
|
|
1951
|
+
max_attempts: { type: "integer", default: 3 },
|
|
1952
|
+
last_attempt_at: { type: "datetime", nullable: true },
|
|
1953
|
+
last_error: { type: "text", nullable: true },
|
|
1954
|
+
expires_at: { type: "datetime", nullable: true },
|
|
1955
|
+
created_at: { type: "datetime", default: "now" },
|
|
1956
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1957
|
+
},
|
|
1958
|
+
indexes: [
|
|
1959
|
+
{ columns: ["tenant_id"] },
|
|
1960
|
+
{ columns: ["device_id"] },
|
|
1961
|
+
{ columns: ["status"] },
|
|
1962
|
+
{ columns: ["priority"] },
|
|
1963
|
+
{ columns: ["expires_at"] },
|
|
1964
|
+
{ columns: ["device_id", "status", "priority"] }
|
|
1965
|
+
]
|
|
1966
|
+
},
|
|
1967
|
+
// ----------------------------------------
|
|
1968
|
+
// Plugin Storage Table
|
|
1969
|
+
// ----------------------------------------
|
|
1970
|
+
mdm_plugin_storage: {
|
|
1971
|
+
columns: {
|
|
1972
|
+
plugin_name: { type: "string" },
|
|
1973
|
+
key: { type: "string" },
|
|
1974
|
+
value: { type: "json" },
|
|
1975
|
+
created_at: { type: "datetime", default: "now" },
|
|
1976
|
+
updated_at: { type: "datetime", default: "now" }
|
|
1977
|
+
},
|
|
1978
|
+
indexes: [
|
|
1979
|
+
{ columns: ["plugin_name", "key"], unique: true },
|
|
1980
|
+
{ columns: ["plugin_name"] }
|
|
1981
|
+
]
|
|
589
1982
|
}
|
|
590
1983
|
}
|
|
591
1984
|
};
|
|
@@ -632,6 +2025,13 @@ function createMDM(config) {
|
|
|
632
2025
|
const eventHandlers = /* @__PURE__ */ new Map();
|
|
633
2026
|
const pushAdapter = push ? createPushAdapter(push, database) : createStubPushAdapter();
|
|
634
2027
|
const webhookManager = webhooksConfig ? createWebhookManager(webhooksConfig) : void 0;
|
|
2028
|
+
const tenantManager = config.multiTenancy?.enabled ? createTenantManager(database) : void 0;
|
|
2029
|
+
const authorizationManager = config.authorization?.enabled ? createAuthorizationManager(database) : void 0;
|
|
2030
|
+
const auditManager = config.audit?.enabled ? createAuditManager(database) : void 0;
|
|
2031
|
+
const scheduleManager = config.scheduling?.enabled ? createScheduleManager(database) : void 0;
|
|
2032
|
+
const messageQueueManager = database.enqueueMessage ? createMessageQueueManager(database) : void 0;
|
|
2033
|
+
const dashboardManager = createDashboardManager(database);
|
|
2034
|
+
const pluginStorageAdapter = config.pluginStorage?.adapter === "database" ? createPluginStorageAdapter(database) : config.pluginStorage?.adapter === "memory" ? createMemoryPluginStorageAdapter() : void 0;
|
|
635
2035
|
const on = (event, handler) => {
|
|
636
2036
|
if (!eventHandlers.has(event)) {
|
|
637
2037
|
eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1034,6 +2434,110 @@ function createMDM(config) {
|
|
|
1034
2434
|
async getChildren(groupId) {
|
|
1035
2435
|
const allGroups = await database.listGroups();
|
|
1036
2436
|
return allGroups.filter((g) => g.parentId === groupId);
|
|
2437
|
+
},
|
|
2438
|
+
async getTree(rootId) {
|
|
2439
|
+
if (database.getGroupTree) {
|
|
2440
|
+
return database.getGroupTree(rootId);
|
|
2441
|
+
}
|
|
2442
|
+
const allGroups = await database.listGroups();
|
|
2443
|
+
new Map(allGroups.map((g) => [g.id, g]));
|
|
2444
|
+
const buildNode = (group, depth, path) => {
|
|
2445
|
+
const children = allGroups.filter((g) => g.parentId === group.id).map((child) => buildNode(child, depth + 1, [...path, group.id]));
|
|
2446
|
+
return {
|
|
2447
|
+
...group,
|
|
2448
|
+
children,
|
|
2449
|
+
depth,
|
|
2450
|
+
path,
|
|
2451
|
+
effectivePolicyId: group.policyId
|
|
2452
|
+
};
|
|
2453
|
+
};
|
|
2454
|
+
const roots = allGroups.filter(
|
|
2455
|
+
(g) => rootId ? g.id === rootId : !g.parentId
|
|
2456
|
+
);
|
|
2457
|
+
return roots.map((root) => buildNode(root, 0, []));
|
|
2458
|
+
},
|
|
2459
|
+
async getAncestors(groupId) {
|
|
2460
|
+
if (database.getGroupAncestors) {
|
|
2461
|
+
return database.getGroupAncestors(groupId);
|
|
2462
|
+
}
|
|
2463
|
+
const ancestors = [];
|
|
2464
|
+
const allGroups = await database.listGroups();
|
|
2465
|
+
const groupMap = new Map(allGroups.map((g) => [g.id, g]));
|
|
2466
|
+
let current = groupMap.get(groupId);
|
|
2467
|
+
while (current?.parentId) {
|
|
2468
|
+
const parent = groupMap.get(current.parentId);
|
|
2469
|
+
if (parent) {
|
|
2470
|
+
ancestors.push(parent);
|
|
2471
|
+
current = parent;
|
|
2472
|
+
} else {
|
|
2473
|
+
break;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return ancestors;
|
|
2477
|
+
},
|
|
2478
|
+
async getDescendants(groupId) {
|
|
2479
|
+
if (database.getGroupDescendants) {
|
|
2480
|
+
return database.getGroupDescendants(groupId);
|
|
2481
|
+
}
|
|
2482
|
+
const allGroups = await database.listGroups();
|
|
2483
|
+
const descendants = [];
|
|
2484
|
+
const findDescendants = (parentId) => {
|
|
2485
|
+
const children = allGroups.filter((g) => g.parentId === parentId);
|
|
2486
|
+
for (const child of children) {
|
|
2487
|
+
descendants.push(child);
|
|
2488
|
+
findDescendants(child.id);
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
findDescendants(groupId);
|
|
2492
|
+
return descendants;
|
|
2493
|
+
},
|
|
2494
|
+
async move(groupId, newParentId) {
|
|
2495
|
+
if (newParentId) {
|
|
2496
|
+
const ancestors = await this.getAncestors(newParentId);
|
|
2497
|
+
if (ancestors.some((a) => a.id === groupId)) {
|
|
2498
|
+
throw new Error("Cannot move group: would create circular reference");
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return database.updateGroup(groupId, { parentId: newParentId });
|
|
2502
|
+
},
|
|
2503
|
+
async getEffectivePolicy(groupId) {
|
|
2504
|
+
if (database.getGroupEffectivePolicy) {
|
|
2505
|
+
return database.getGroupEffectivePolicy(groupId);
|
|
2506
|
+
}
|
|
2507
|
+
const group = await database.findGroup(groupId);
|
|
2508
|
+
if (!group) return null;
|
|
2509
|
+
if (group.policyId) {
|
|
2510
|
+
return database.findPolicy(group.policyId);
|
|
2511
|
+
}
|
|
2512
|
+
const ancestors = await this.getAncestors(groupId);
|
|
2513
|
+
for (const ancestor of ancestors) {
|
|
2514
|
+
if (ancestor.policyId) {
|
|
2515
|
+
return database.findPolicy(ancestor.policyId);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return null;
|
|
2519
|
+
},
|
|
2520
|
+
async getHierarchyStats() {
|
|
2521
|
+
if (database.getGroupHierarchyStats) {
|
|
2522
|
+
return database.getGroupHierarchyStats();
|
|
2523
|
+
}
|
|
2524
|
+
const allGroups = await database.listGroups();
|
|
2525
|
+
let maxDepth = 0;
|
|
2526
|
+
let groupsWithDevices = 0;
|
|
2527
|
+
let groupsWithPolicies = 0;
|
|
2528
|
+
for (const group of allGroups) {
|
|
2529
|
+
const ancestors = await this.getAncestors(group.id);
|
|
2530
|
+
maxDepth = Math.max(maxDepth, ancestors.length);
|
|
2531
|
+
const devices2 = await database.listDevicesInGroup(group.id);
|
|
2532
|
+
if (devices2.length > 0) groupsWithDevices++;
|
|
2533
|
+
if (group.policyId) groupsWithPolicies++;
|
|
2534
|
+
}
|
|
2535
|
+
return {
|
|
2536
|
+
totalGroups: allGroups.length,
|
|
2537
|
+
maxDepth,
|
|
2538
|
+
groupsWithDevices,
|
|
2539
|
+
groupsWithPolicies
|
|
2540
|
+
};
|
|
1037
2541
|
}
|
|
1038
2542
|
};
|
|
1039
2543
|
const enroll = async (request) => {
|
|
@@ -1245,7 +2749,15 @@ function createMDM(config) {
|
|
|
1245
2749
|
processHeartbeat,
|
|
1246
2750
|
verifyDeviceToken,
|
|
1247
2751
|
getPlugins,
|
|
1248
|
-
getPlugin
|
|
2752
|
+
getPlugin,
|
|
2753
|
+
// Enterprise managers (optional)
|
|
2754
|
+
tenants: tenantManager,
|
|
2755
|
+
authorization: authorizationManager,
|
|
2756
|
+
audit: auditManager,
|
|
2757
|
+
schedules: scheduleManager,
|
|
2758
|
+
messageQueue: messageQueueManager,
|
|
2759
|
+
dashboard: dashboardManager,
|
|
2760
|
+
pluginStorage: pluginStorageAdapter
|
|
1249
2761
|
};
|
|
1250
2762
|
(async () => {
|
|
1251
2763
|
for (const plugin of plugins) {
|
|
@@ -1363,6 +2875,6 @@ function generateDeviceToken(deviceId, secret, expirationSeconds) {
|
|
|
1363
2875
|
return `${header}.${payload}.${signature}`;
|
|
1364
2876
|
}
|
|
1365
2877
|
|
|
1366
|
-
export { ApplicationNotFoundError, AuthenticationError, AuthorizationError, DeviceNotFoundError, EnrollmentError, MDMError, PolicyNotFoundError, ValidationError, camelToSnake, createMDM, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyWebhookSignature };
|
|
2878
|
+
export { ApplicationNotFoundError, AuthenticationError, AuthorizationError, DeviceNotFoundError, EnrollmentError, GroupNotFoundError, MDMError, PolicyNotFoundError, RoleNotFoundError, TenantNotFoundError, UserNotFoundError, ValidationError, camelToSnake, createAuditManager, createAuthorizationManager, createDashboardManager, createMDM, createMemoryPluginStorageAdapter, createMessageQueueManager, createPluginKey, createPluginStorageAdapter, createScheduleManager, createTenantManager, createWebhookManager, getColumnNames, getPrimaryKey, getTableNames, mdmSchema, parsePluginKey, snakeToCamel, transformToCamelCase, transformToSnakeCase, verifyWebhookSignature };
|
|
1367
2879
|
//# sourceMappingURL=index.js.map
|
|
1368
2880
|
//# sourceMappingURL=index.js.map
|