feat(reception): add passport device authentication flows and alert delivery management
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import { Alert } from '../ts/reception/classes.alert.js';
|
||||
import { AlertManager } from '../ts/reception/classes.alertmanager.js';
|
||||
import { AlertRule } from '../ts/reception/classes.alertrule.js';
|
||||
import { PassportDevice } from '../ts/reception/classes.passportdevice.js';
|
||||
import { Role } from '../ts/reception/classes.role.js';
|
||||
import { User } from '../ts/reception/classes.user.js';
|
||||
|
||||
const getNestedValue = (targetArg: any, pathArg: string) => {
|
||||
return pathArg.split('.').reduce((currentArg, keyArg) => currentArg?.[keyArg], targetArg);
|
||||
};
|
||||
|
||||
const matchesQuery = (targetArg: any, queryArg: Record<string, any>) => {
|
||||
return Object.entries(queryArg).every(([keyArg, valueArg]) => getNestedValue(targetArg, keyArg) === valueArg);
|
||||
};
|
||||
|
||||
const createTestAlertManager = () => {
|
||||
const alerts = new Map<string, Alert>();
|
||||
const alertRules = new Map<string, AlertRule>();
|
||||
const users = new Map<string, User>();
|
||||
const roles = new Map<string, Role>();
|
||||
const passportDevices = new Map<string, PassportDevice>();
|
||||
const deliveredHints: string[] = [];
|
||||
|
||||
const manager = new AlertManager({
|
||||
db: { smartdataDb: {} },
|
||||
typedrouter: { addTypedRouter: () => undefined },
|
||||
jwtManager: {
|
||||
verifyJWTAndGetData: async (jwtArg: string) => ({
|
||||
data: {
|
||||
userId: jwtArg,
|
||||
},
|
||||
}),
|
||||
},
|
||||
userManager: {
|
||||
CUser: {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(users.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
getInstances: async () => Array.from(users.values()),
|
||||
},
|
||||
},
|
||||
roleManager: {
|
||||
CRole: {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(roles.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
},
|
||||
getAllRolesForOrg: async (organizationIdArg: string) =>
|
||||
Array.from(roles.values()).filter((roleArg) => roleArg.data.organizationId === organizationIdArg),
|
||||
},
|
||||
passportManager: {
|
||||
authenticatePassportDeviceRequest: async (requestArg: { deviceId: string }) => {
|
||||
return passportDevices.get(requestArg.deviceId)!;
|
||||
},
|
||||
getPassportDevicesForUser: async (userIdArg: string) =>
|
||||
Array.from(passportDevices.values()).filter(
|
||||
(deviceArg) => deviceArg.data.userId === userIdArg && deviceArg.data.status === 'active'
|
||||
),
|
||||
},
|
||||
passportPushManager: {
|
||||
deliverAlertHint: async (_passportDeviceArg: PassportDevice, alertArg: Alert) => {
|
||||
deliveredHints.push(alertArg.data.notification.hintId);
|
||||
alertArg.data.notification = {
|
||||
...alertArg.data.notification,
|
||||
status: 'sent',
|
||||
attemptCount: alertArg.data.notification.attemptCount + 1,
|
||||
deliveredAt: Date.now(),
|
||||
lastError: null,
|
||||
};
|
||||
await alertArg.save();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const originalAlertSave = Alert.prototype.save;
|
||||
const originalAlertDelete = Alert.prototype.delete;
|
||||
const originalAlertRuleSave = AlertRule.prototype.save;
|
||||
const originalAlertRuleDelete = AlertRule.prototype.delete;
|
||||
|
||||
(Alert.prototype as Alert & { save: () => Promise<void> }).save = async function () {
|
||||
alerts.set(this.id, this);
|
||||
};
|
||||
(Alert.prototype as Alert & { delete: () => Promise<void> }).delete = async function () {
|
||||
alerts.delete(this.id);
|
||||
};
|
||||
(AlertRule.prototype as AlertRule & { save: () => Promise<void> }).save = async function () {
|
||||
alertRules.set(this.id, this);
|
||||
};
|
||||
(AlertRule.prototype as AlertRule & { delete: () => Promise<void> }).delete = async function () {
|
||||
alertRules.delete(this.id);
|
||||
};
|
||||
|
||||
(manager as any).CAlert = {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(alerts.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
getInstances: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(alerts.values()).filter((docArg) => matchesQuery(docArg, queryArg));
|
||||
},
|
||||
};
|
||||
(manager as any).CAlertRule = {
|
||||
getInstance: async (queryArg: Record<string, any>) => {
|
||||
return Array.from(alertRules.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
||||
},
|
||||
getInstances: async () => Array.from(alertRules.values()),
|
||||
};
|
||||
|
||||
return {
|
||||
manager,
|
||||
alerts,
|
||||
alertRules,
|
||||
users,
|
||||
roles,
|
||||
passportDevices,
|
||||
deliveredHints,
|
||||
restore: () => {
|
||||
Alert.prototype.save = originalAlertSave;
|
||||
Alert.prototype.delete = originalAlertDelete;
|
||||
AlertRule.prototype.save = originalAlertRuleSave;
|
||||
AlertRule.prototype.delete = originalAlertRuleDelete;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const addUser = (
|
||||
usersArg: Map<string, User>,
|
||||
optionsArg: { id: string; email: string; isGlobalAdmin?: boolean }
|
||||
) => {
|
||||
const user = new User();
|
||||
user.id = optionsArg.id;
|
||||
user.data = {
|
||||
name: optionsArg.email,
|
||||
username: optionsArg.email,
|
||||
email: optionsArg.email,
|
||||
status: 'active',
|
||||
connectedOrgs: [],
|
||||
isGlobalAdmin: optionsArg.isGlobalAdmin,
|
||||
};
|
||||
usersArg.set(user.id, user);
|
||||
return user;
|
||||
};
|
||||
|
||||
const addPassportDevice = (
|
||||
passportDevicesArg: Map<string, PassportDevice>,
|
||||
optionsArg: { id: string; userId: string; label: string }
|
||||
) => {
|
||||
const device = new PassportDevice();
|
||||
device.id = optionsArg.id;
|
||||
device.data = {
|
||||
userId: optionsArg.userId,
|
||||
label: optionsArg.label,
|
||||
platform: 'ios',
|
||||
status: 'active',
|
||||
publicKeyAlgorithm: 'p256',
|
||||
publicKeyX963Base64: 'public-key',
|
||||
capabilities: {
|
||||
gps: true,
|
||||
nfc: true,
|
||||
push: true,
|
||||
},
|
||||
pushRegistration: {
|
||||
provider: 'apns',
|
||||
token: `${optionsArg.id}-token`,
|
||||
topic: 'global.idp.swiftapp',
|
||||
environment: 'development',
|
||||
registeredAt: Date.now(),
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
lastSeenAt: Date.now(),
|
||||
};
|
||||
passportDevicesArg.set(device.id, device);
|
||||
return device;
|
||||
};
|
||||
|
||||
tap.test('creates global admin access alerts with the built-in fallback rule', async () => {
|
||||
const { manager, users, passportDevices, alerts, deliveredHints, restore } = createTestAlertManager();
|
||||
|
||||
try {
|
||||
addUser(users, { id: 'admin-1', email: 'admin-1@example.com', isGlobalAdmin: true });
|
||||
addPassportDevice(passportDevices, { id: 'device-1', userId: 'admin-1', label: 'Admin Phone' });
|
||||
|
||||
const createdAlerts = await manager.createAlertsForEvent({
|
||||
category: 'admin',
|
||||
eventType: 'global_admin_access',
|
||||
severity: 'high',
|
||||
title: 'Global admin console accessed',
|
||||
body: 'A global admin accessed the console.',
|
||||
actorUserId: 'admin-1',
|
||||
relatedEntityType: 'global-admin-console',
|
||||
});
|
||||
|
||||
expect(createdAlerts).toHaveLength(1);
|
||||
expect(alerts.size).toEqual(1);
|
||||
expect(createdAlerts[0].data.notification.status).toEqual('sent');
|
||||
expect(deliveredHints).toHaveLength(1);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('routes organization-scoped alerts to org admins by rule', async () => {
|
||||
const { manager, users, roles, passportDevices, restore } = createTestAlertManager();
|
||||
|
||||
try {
|
||||
addUser(users, { id: 'owner-1', email: 'owner@example.com' });
|
||||
addUser(users, { id: 'viewer-1', email: 'viewer@example.com' });
|
||||
addPassportDevice(passportDevices, { id: 'owner-device', userId: 'owner-1', label: 'Owner Phone' });
|
||||
|
||||
const ownerRole = new Role();
|
||||
ownerRole.id = 'role-owner';
|
||||
ownerRole.data = {
|
||||
userId: 'owner-1',
|
||||
organizationId: 'org-1',
|
||||
roles: ['owner'],
|
||||
};
|
||||
roles.set(ownerRole.id, ownerRole);
|
||||
|
||||
const viewerRole = new Role();
|
||||
viewerRole.id = 'role-viewer';
|
||||
viewerRole.data = {
|
||||
userId: 'viewer-1',
|
||||
organizationId: 'org-1',
|
||||
roles: ['viewer'],
|
||||
};
|
||||
roles.set(viewerRole.id, viewerRole);
|
||||
|
||||
const rule = new AlertRule();
|
||||
rule.id = 'org-admin-rule';
|
||||
rule.data = {
|
||||
scope: 'organization',
|
||||
organizationId: 'org-1',
|
||||
eventType: 'org_security_notice',
|
||||
minimumSeverity: 'medium',
|
||||
recipientMode: 'org_admins',
|
||||
recipientUserIds: [],
|
||||
push: true,
|
||||
enabled: true,
|
||||
createdByUserId: 'owner-1',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await rule.save();
|
||||
|
||||
const createdAlerts = await manager.createAlertsForEvent({
|
||||
category: 'security',
|
||||
eventType: 'org_security_notice',
|
||||
severity: 'high',
|
||||
title: 'Organization security event',
|
||||
body: 'A sensitive organization event occurred.',
|
||||
actorUserId: 'viewer-1',
|
||||
organizationId: 'org-1',
|
||||
});
|
||||
|
||||
expect(createdAlerts).toHaveLength(1);
|
||||
expect(createdAlerts[0].data.recipientUserId).toEqual('owner-1');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('lists alerts, resolves hint lookups, and marks alerts seen', async () => {
|
||||
const { manager, alerts, restore } = createTestAlertManager();
|
||||
|
||||
try {
|
||||
const alert = new Alert();
|
||||
alert.id = 'alert-1';
|
||||
alert.data = {
|
||||
recipientUserId: 'user-1',
|
||||
category: 'security',
|
||||
eventType: 'global_admin_access',
|
||||
severity: 'high',
|
||||
title: 'Important alert',
|
||||
body: 'Please inspect this alert.',
|
||||
notification: {
|
||||
hintId: 'hint-1',
|
||||
status: 'sent',
|
||||
attemptCount: 1,
|
||||
createdAt: Date.now(),
|
||||
deliveredAt: Date.now(),
|
||||
seenAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
seenAt: null,
|
||||
dismissedAt: null,
|
||||
};
|
||||
await alert.save();
|
||||
|
||||
const listedAlerts = await manager.listAlertsForUser('user-1');
|
||||
expect(listedAlerts).toHaveLength(1);
|
||||
|
||||
const hintAlert = await manager.getAlertByHint('user-1', 'hint-1');
|
||||
expect(hintAlert?.id).toEqual('alert-1');
|
||||
|
||||
const seenAlert = await manager.markAlertSeen('user-1', 'hint-1');
|
||||
expect(seenAlert.data.notification.status).toEqual('seen');
|
||||
expect(seenAlert.data.seenAt).toBeGreaterThan(0);
|
||||
expect(alerts.get('alert-1')?.data.notification.status).toEqual('seen');
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user