add office-aware passport policies and alert lifecycle
Enforce geofenced location evidence for passport challenges and extend admin alerting so mobile devices can review, dismiss, and act on real org and security events.
This commit is contained in:
@@ -261,6 +261,42 @@ tap.test('routes organization-scoped alerts to org admins by rule', async () =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('uses built-in organization fallback rules for app connection events', async () => {
|
||||||
|
const { manager, users, roles, passportDevices, deliveredHints, restore } = createTestAlertManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
addUser(users, { id: 'owner-1', email: 'owner@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 createdAlerts = await manager.createAlertsForEvent({
|
||||||
|
category: 'admin',
|
||||||
|
eventType: 'org_app_connected',
|
||||||
|
severity: 'medium',
|
||||||
|
title: 'Organization app connected',
|
||||||
|
body: 'A new app was connected.',
|
||||||
|
actorUserId: 'owner-1',
|
||||||
|
organizationId: 'org-1',
|
||||||
|
relatedEntityId: 'app-1',
|
||||||
|
relatedEntityType: 'global-app',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createdAlerts).toHaveLength(1);
|
||||||
|
expect(createdAlerts[0].data.recipientUserId).toEqual('owner-1');
|
||||||
|
expect(deliveredHints).toHaveLength(1);
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('lists alerts, resolves hint lookups, and marks alerts seen', async () => {
|
tap.test('lists alerts, resolves hint lookups, and marks alerts seen', async () => {
|
||||||
const { manager, alerts, restore } = createTestAlertManager();
|
const { manager, alerts, restore } = createTestAlertManager();
|
||||||
|
|
||||||
@@ -299,6 +335,15 @@ tap.test('lists alerts, resolves hint lookups, and marks alerts seen', async ()
|
|||||||
expect(seenAlert.data.notification.status).toEqual('seen');
|
expect(seenAlert.data.notification.status).toEqual('seen');
|
||||||
expect(seenAlert.data.seenAt).toBeGreaterThan(0);
|
expect(seenAlert.data.seenAt).toBeGreaterThan(0);
|
||||||
expect(alerts.get('alert-1')?.data.notification.status).toEqual('seen');
|
expect(alerts.get('alert-1')?.data.notification.status).toEqual('seen');
|
||||||
|
|
||||||
|
const dismissedAlert = await manager.dismissAlert('user-1', 'hint-1');
|
||||||
|
expect(dismissedAlert.data.dismissedAt).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const defaultList = await manager.listAlertsForUser('user-1');
|
||||||
|
expect(defaultList).toHaveLength(0);
|
||||||
|
|
||||||
|
const fullList = await manager.listAlertsForUser('user-1', true);
|
||||||
|
expect(fullList).toHaveLength(1);
|
||||||
} finally {
|
} finally {
|
||||||
restore();
|
restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,14 @@ tap.test('creates and approves a passport challenge with DER signatures and evid
|
|||||||
notificationTitle: 'Office entry request',
|
notificationTitle: 'Office entry request',
|
||||||
requireLocation: true,
|
requireLocation: true,
|
||||||
requireNfc: true,
|
requireNfc: true,
|
||||||
|
locationPolicy: {
|
||||||
|
mode: 'geofence',
|
||||||
|
label: 'HQ Berlin',
|
||||||
|
latitude: 53.0793,
|
||||||
|
longitude: 8.8017,
|
||||||
|
radiusMeters: 80,
|
||||||
|
maxAccuracyMeters: 25,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(deliveredHintIds).toHaveLength(1);
|
expect(deliveredHintIds).toHaveLength(1);
|
||||||
@@ -286,6 +294,15 @@ tap.test('creates and approves a passport challenge with DER signatures and evid
|
|||||||
deviceId: passportDevice.id,
|
deviceId: passportDevice.id,
|
||||||
signatureBase64: signer.sign(challengeResult.signingPayload),
|
signatureBase64: signer.sign(challengeResult.signingPayload),
|
||||||
signatureFormat: 'der',
|
signatureFormat: 'der',
|
||||||
|
location: {
|
||||||
|
latitude: 53.5,
|
||||||
|
longitude: 8.1,
|
||||||
|
accuracyMeters: 12,
|
||||||
|
capturedAt: Date.now(),
|
||||||
|
},
|
||||||
|
nfc: {
|
||||||
|
readerId: 'door-reader-a',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
|
|
||||||
@@ -308,6 +325,7 @@ tap.test('creates and approves a passport challenge with DER signatures and evid
|
|||||||
expect(approvedChallenge.data.status).toEqual('approved');
|
expect(approvedChallenge.data.status).toEqual('approved');
|
||||||
expect(approvedChallenge.data.evidence?.signatureFormat).toEqual('der');
|
expect(approvedChallenge.data.evidence?.signatureFormat).toEqual('der');
|
||||||
expect(approvedChallenge.data.evidence?.location?.accuracyMeters).toEqual(12);
|
expect(approvedChallenge.data.evidence?.location?.accuracyMeters).toEqual(12);
|
||||||
|
expect(approvedChallenge.data.evidence?.locationEvaluation?.matched).toBeTrue();
|
||||||
expect(approvedChallenge.data.evidence?.nfc?.readerId).toEqual('door-reader-a');
|
expect(approvedChallenge.data.evidence?.nfc?.readerId).toEqual('door-reader-a');
|
||||||
expect(activityLogCalls.at(-1)?.action).toEqual('passport_challenge_approved');
|
expect(activityLogCalls.at(-1)?.action).toEqual('passport_challenge_approved');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export class AlertManager {
|
|||||||
action: 'listPassportAlerts',
|
action: 'listPassportAlerts',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const alerts = await this.listAlertsForUser(passportDevice.data.userId);
|
const alerts = await this.listAlertsForUser(
|
||||||
|
passportDevice.data.userId,
|
||||||
|
!!requestArg.includeDismissed
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
alerts: alerts.map((alertArg) => ({ id: alertArg.id, data: alertArg.data })),
|
alerts: alerts.map((alertArg) => ({ id: alertArg.id, data: alertArg.data })),
|
||||||
};
|
};
|
||||||
@@ -82,6 +85,25 @@ export class AlertManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_DismissPassportAlert>(
|
||||||
|
'dismissPassportAlert',
|
||||||
|
async (requestArg) => {
|
||||||
|
const passportDevice = await this.receptionRef.passportManager.authenticatePassportDeviceRequest(
|
||||||
|
requestArg,
|
||||||
|
{
|
||||||
|
action: 'dismissPassportAlert',
|
||||||
|
signedFields: [`hint_id=${requestArg.hintId}`],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await this.dismissAlert(passportDevice.data.userId, requestArg.hintId);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.typedRouter.addTypedHandler(
|
this.typedRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpsertAlertRule>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpsertAlertRule>(
|
||||||
'upsertAlertRule',
|
'upsertAlertRule',
|
||||||
@@ -263,33 +285,71 @@ export class AlertManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (optionsArg.eventType === 'global_admin_access') {
|
if (optionsArg.eventType === 'global_admin_access') {
|
||||||
const fallbackRule = new AlertRule();
|
return [this.createBuiltInRule('builtin-global-admin-access', {
|
||||||
fallbackRule.id = 'builtin-global-admin-access';
|
|
||||||
fallbackRule.data = {
|
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
organizationId: undefined,
|
|
||||||
eventType: 'global_admin_access',
|
eventType: 'global_admin_access',
|
||||||
minimumSeverity: 'high',
|
minimumSeverity: 'high',
|
||||||
recipientMode: 'global_admins',
|
recipientMode: 'global_admins',
|
||||||
recipientUserIds: [],
|
})];
|
||||||
push: true,
|
|
||||||
enabled: true,
|
|
||||||
createdByUserId: 'system',
|
|
||||||
createdAt: 0,
|
|
||||||
updatedAt: 0,
|
|
||||||
};
|
|
||||||
return [fallbackRule];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionsArg.eventType === 'global_app_credentials_regenerated') {
|
if (optionsArg.eventType === 'global_app_credentials_regenerated') {
|
||||||
const fallbackRule = new AlertRule();
|
return [this.createBuiltInRule('builtin-global-app-credentials-regenerated', {
|
||||||
fallbackRule.id = 'builtin-global-app-credentials-regenerated';
|
|
||||||
fallbackRule.data = {
|
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
organizationId: undefined,
|
|
||||||
eventType: 'global_app_credentials_regenerated',
|
eventType: 'global_app_credentials_regenerated',
|
||||||
minimumSeverity: 'critical',
|
minimumSeverity: 'critical',
|
||||||
recipientMode: 'global_admins',
|
recipientMode: 'global_admins',
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionsArg.organizationId) {
|
||||||
|
const organizationFallbackMap: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
minimumSeverity: plugins.idpInterfaces.data.TAlertSeverity;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
org_app_connected: { minimumSeverity: 'medium' },
|
||||||
|
org_app_disconnected: { minimumSeverity: 'medium' },
|
||||||
|
org_invitation_created: { minimumSeverity: 'low' },
|
||||||
|
org_invitation_resent: { minimumSeverity: 'low' },
|
||||||
|
org_member_removed: { minimumSeverity: 'high' },
|
||||||
|
org_member_roles_updated: { minimumSeverity: 'high' },
|
||||||
|
org_ownership_transferred: { minimumSeverity: 'critical' },
|
||||||
|
};
|
||||||
|
const fallbackConfig = organizationFallbackMap[optionsArg.eventType];
|
||||||
|
if (fallbackConfig) {
|
||||||
|
return [this.createBuiltInRule(`builtin-${optionsArg.eventType}`, {
|
||||||
|
scope: 'organization',
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
eventType: optionsArg.eventType,
|
||||||
|
minimumSeverity: fallbackConfig.minimumSeverity,
|
||||||
|
recipientMode: 'org_admins',
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBuiltInRule(
|
||||||
|
ruleIdArg: string,
|
||||||
|
optionsArg: {
|
||||||
|
scope: plugins.idpInterfaces.data.TAlertRuleScope;
|
||||||
|
organizationId?: string;
|
||||||
|
eventType: string;
|
||||||
|
minimumSeverity: plugins.idpInterfaces.data.TAlertSeverity;
|
||||||
|
recipientMode: plugins.idpInterfaces.data.TAlertRuleRecipientMode;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const fallbackRule = new AlertRule();
|
||||||
|
fallbackRule.id = ruleIdArg;
|
||||||
|
fallbackRule.data = {
|
||||||
|
scope: optionsArg.scope,
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
eventType: optionsArg.eventType,
|
||||||
|
minimumSeverity: optionsArg.minimumSeverity,
|
||||||
|
recipientMode: optionsArg.recipientMode,
|
||||||
recipientUserIds: [],
|
recipientUserIds: [],
|
||||||
push: true,
|
push: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -297,10 +357,7 @@ export class AlertManager {
|
|||||||
createdAt: 0,
|
createdAt: 0,
|
||||||
updatedAt: 0,
|
updatedAt: 0,
|
||||||
};
|
};
|
||||||
return [fallbackRule];
|
return fallbackRule;
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAlertsForEvent(optionsArg: {
|
public async createAlertsForEvent(optionsArg: {
|
||||||
@@ -378,11 +435,13 @@ export class AlertManager {
|
|||||||
return createdAlerts;
|
return createdAlerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listAlertsForUser(userIdArg: string) {
|
public async listAlertsForUser(userIdArg: string, includeDismissedArg = false) {
|
||||||
const alerts = await this.CAlert.getInstances({
|
const alerts = await this.CAlert.getInstances({
|
||||||
'data.recipientUserId': userIdArg,
|
'data.recipientUserId': userIdArg,
|
||||||
});
|
});
|
||||||
return alerts.sort((leftArg, rightArg) => rightArg.data.createdAt - leftArg.data.createdAt);
|
return alerts
|
||||||
|
.filter((alertArg) => includeDismissedArg || !alertArg.data.dismissedAt)
|
||||||
|
.sort((leftArg, rightArg) => rightArg.data.createdAt - leftArg.data.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAlertByHint(userIdArg: string, hintIdArg: string) {
|
public async getAlertByHint(userIdArg: string, hintIdArg: string) {
|
||||||
@@ -408,6 +467,25 @@ export class AlertManager {
|
|||||||
return alert;
|
return alert;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async dismissAlert(userIdArg: string, hintIdArg: string) {
|
||||||
|
const alert = await this.getAlertByHint(userIdArg, hintIdArg);
|
||||||
|
if (!alert) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Alert not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.data.dismissedAt = Date.now();
|
||||||
|
if (!alert.data.seenAt) {
|
||||||
|
alert.data.seenAt = Date.now();
|
||||||
|
}
|
||||||
|
alert.data.notification = {
|
||||||
|
...alert.data.notification,
|
||||||
|
status: 'seen',
|
||||||
|
seenAt: alert.data.notification.seenAt || Date.now(),
|
||||||
|
};
|
||||||
|
await alert.save();
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
public async reDeliverPendingAlerts() {
|
public async reDeliverPendingAlerts() {
|
||||||
const alerts = await this.CAlert.getInstances({});
|
const alerts = await this.CAlert.getInstances({});
|
||||||
for (const alert of alerts) {
|
for (const alert of alerts) {
|
||||||
|
|||||||
@@ -11,6 +11,29 @@ export class AppConnectionManager {
|
|||||||
|
|
||||||
public CAppConnection = plugins.smartdata.setDefaultManagerForDoc(this, AppConnection);
|
public CAppConnection = plugins.smartdata.setDefaultManagerForDoc(this, AppConnection);
|
||||||
|
|
||||||
|
private async emitOrganizationAlert(optionsArg: {
|
||||||
|
organizationId: string;
|
||||||
|
eventType: string;
|
||||||
|
severity: plugins.idpInterfaces.data.TAlertSeverity;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
actorUserId: string;
|
||||||
|
relatedEntityId?: string;
|
||||||
|
relatedEntityType?: string;
|
||||||
|
}) {
|
||||||
|
await this.receptionRef.alertManager.createAlertsForEvent({
|
||||||
|
category: 'admin',
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
eventType: optionsArg.eventType,
|
||||||
|
severity: optionsArg.severity,
|
||||||
|
title: optionsArg.title,
|
||||||
|
body: optionsArg.body,
|
||||||
|
actorUserId: optionsArg.actorUserId,
|
||||||
|
relatedEntityId: optionsArg.relatedEntityId,
|
||||||
|
relatedEntityType: optionsArg.relatedEntityType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
constructor(receptionRefArg: Reception) {
|
constructor(receptionRefArg: Reception) {
|
||||||
this.receptionRef = receptionRefArg;
|
this.receptionRef = receptionRefArg;
|
||||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
@@ -131,6 +154,17 @@ export class AppConnectionManager {
|
|||||||
await connection.save();
|
await connection.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_app_connected',
|
||||||
|
severity: 'medium',
|
||||||
|
title: 'Organization app connected',
|
||||||
|
body: `${user.data.email} connected ${app.data.name} to this organization.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: app.id,
|
||||||
|
relatedEntityType: 'global-app',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
connection: await connection.createSavableObject(),
|
connection: await connection.createSavableObject(),
|
||||||
@@ -145,6 +179,17 @@ export class AppConnectionManager {
|
|||||||
|
|
||||||
await connection.disconnect();
|
await connection.disconnect();
|
||||||
|
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_app_disconnected',
|
||||||
|
severity: 'medium',
|
||||||
|
title: 'Organization app disconnected',
|
||||||
|
body: `${user.data.email} disconnected ${app.data.name} from this organization.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: app.id,
|
||||||
|
relatedEntityType: 'global-app',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
connection: await connection.createSavableObject(),
|
connection: await connection.createSavableObject(),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class PassportChallenge extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
deviceLabel: undefined,
|
deviceLabel: undefined,
|
||||||
requireLocation: false,
|
requireLocation: false,
|
||||||
requireNfc: false,
|
requireNfc: false,
|
||||||
|
locationPolicy: undefined,
|
||||||
requestedCapabilities: undefined,
|
requestedCapabilities: undefined,
|
||||||
},
|
},
|
||||||
evidence: undefined,
|
evidence: undefined,
|
||||||
@@ -56,4 +57,10 @@ export class PassportChallenge extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
this.data.status = 'expired';
|
this.data.status = 'expired';
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async markRejected() {
|
||||||
|
this.data.status = 'rejected';
|
||||||
|
this.data.completedAt = Date.now();
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,47 @@ export class PassportManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetPassportDashboard>(
|
||||||
|
'getPassportDashboard',
|
||||||
|
async (requestArg) => {
|
||||||
|
const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, {
|
||||||
|
action: 'getPassportDashboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: passportDevice.data.userId,
|
||||||
|
});
|
||||||
|
const organizations = user
|
||||||
|
? await this.receptionRef.organizationmanager.getAllOrganizationsForUser(user)
|
||||||
|
: [];
|
||||||
|
const devices = await this.getPassportDevicesForUser(passportDevice.data.userId);
|
||||||
|
const challenges = await this.listPendingChallengesForDevice(passportDevice.id);
|
||||||
|
const alerts = await this.receptionRef.alertManager.listAlertsForUser(passportDevice.data.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
userId: passportDevice.data.userId,
|
||||||
|
name: user?.data?.name || user?.data?.email || 'Passport User',
|
||||||
|
handle: user?.data?.username || user?.data?.email || passportDevice.data.userId,
|
||||||
|
organizations: organizations.map((organizationArg) => ({
|
||||||
|
id: organizationArg.id,
|
||||||
|
name: organizationArg.data.name,
|
||||||
|
})),
|
||||||
|
deviceCount: devices.length,
|
||||||
|
recoverySummary: 'Recovery workflows are not configured yet for this passport.',
|
||||||
|
},
|
||||||
|
devices: devices.map((deviceArg) => ({ id: deviceArg.id, data: deviceArg.data })),
|
||||||
|
challenges: challenges.map((challengeArg) => ({
|
||||||
|
challenge: { id: challengeArg.id, data: challengeArg.data },
|
||||||
|
signingPayload: this.buildChallengeSigningPayload(challengeArg),
|
||||||
|
})),
|
||||||
|
alerts: alerts.map((alertArg) => ({ id: alertArg.id, data: alertArg.data })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.typedRouter.addTypedHandler(
|
this.typedRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ApprovePassportChallenge>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ApprovePassportChallenge>(
|
||||||
'approvePassportChallenge',
|
'approvePassportChallenge',
|
||||||
@@ -161,6 +202,26 @@ export class PassportManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_RejectPassportChallenge>(
|
||||||
|
'rejectPassportChallenge',
|
||||||
|
async (requestArg) => {
|
||||||
|
const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, {
|
||||||
|
action: 'rejectPassportChallenge',
|
||||||
|
signedFields: [`challenge_id=${requestArg.challengeId}`],
|
||||||
|
});
|
||||||
|
const challenge = await this.rejectPassportChallenge(passportDevice.id, requestArg.challengeId);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
challenge: {
|
||||||
|
id: challenge.id,
|
||||||
|
data: challenge.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
this.typedRouter.addTypedHandler(
|
this.typedRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_RegisterPassportPushToken>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_RegisterPassportPushToken>(
|
||||||
'registerPassportPushToken',
|
'registerPassportPushToken',
|
||||||
@@ -225,8 +286,11 @@ export class PassportManager {
|
|||||||
return {
|
return {
|
||||||
challenge: passportChallenge
|
challenge: passportChallenge
|
||||||
? {
|
? {
|
||||||
|
challenge: {
|
||||||
id: passportChallenge.id,
|
id: passportChallenge.id,
|
||||||
data: passportChallenge.data,
|
data: passportChallenge.data,
|
||||||
|
},
|
||||||
|
signingPayload: this.buildChallengeSigningPayload(passportChallenge),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@@ -374,9 +438,43 @@ export class PassportManager {
|
|||||||
`audience=${challengeArg.data.metadata.audience || ''}`,
|
`audience=${challengeArg.data.metadata.audience || ''}`,
|
||||||
`require_location=${challengeArg.data.metadata.requireLocation}`,
|
`require_location=${challengeArg.data.metadata.requireLocation}`,
|
||||||
`require_nfc=${challengeArg.data.metadata.requireNfc}`,
|
`require_nfc=${challengeArg.data.metadata.requireNfc}`,
|
||||||
|
`location_policy=${challengeArg.data.metadata.locationPolicy ? JSON.stringify(challengeArg.data.metadata.locationPolicy) : ''}`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private evaluateLocationPolicy(
|
||||||
|
locationPolicyArg: plugins.idpInterfaces.data.IPassportLocationPolicy,
|
||||||
|
locationEvidenceArg: plugins.idpInterfaces.data.IPassportLocationEvidence
|
||||||
|
) {
|
||||||
|
const earthRadiusMeters = 6371000;
|
||||||
|
const latitude1 = (locationPolicyArg.latitude * Math.PI) / 180;
|
||||||
|
const latitude2 = (locationEvidenceArg.latitude * Math.PI) / 180;
|
||||||
|
const deltaLatitude = ((locationEvidenceArg.latitude - locationPolicyArg.latitude) * Math.PI) / 180;
|
||||||
|
const deltaLongitude = ((locationEvidenceArg.longitude - locationPolicyArg.longitude) * Math.PI) / 180;
|
||||||
|
|
||||||
|
const haversine =
|
||||||
|
Math.sin(deltaLatitude / 2) * Math.sin(deltaLatitude / 2) +
|
||||||
|
Math.cos(latitude1) * Math.cos(latitude2) * Math.sin(deltaLongitude / 2) * Math.sin(deltaLongitude / 2);
|
||||||
|
const distanceMeters = 2 * earthRadiusMeters * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine));
|
||||||
|
|
||||||
|
const accuracyAccepted =
|
||||||
|
!locationPolicyArg.maxAccuracyMeters ||
|
||||||
|
locationEvidenceArg.accuracyMeters <= locationPolicyArg.maxAccuracyMeters;
|
||||||
|
const withinGeofence = distanceMeters <= locationPolicyArg.radiusMeters;
|
||||||
|
|
||||||
|
return {
|
||||||
|
matched: accuracyAccepted && withinGeofence,
|
||||||
|
distanceMeters,
|
||||||
|
accuracyAccepted,
|
||||||
|
evaluatedAt: Date.now(),
|
||||||
|
reason: !accuracyAccepted
|
||||||
|
? `Accuracy ${locationEvidenceArg.accuracyMeters}m exceeds allowed ${locationPolicyArg.maxAccuracyMeters}m`
|
||||||
|
: !withinGeofence
|
||||||
|
? `Location is ${Math.round(distanceMeters)}m away from ${locationPolicyArg.label || 'required area'}`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private createPairingPayload(
|
private createPairingPayload(
|
||||||
pairingTokenArg: string,
|
pairingTokenArg: string,
|
||||||
challengeArg: PassportChallenge,
|
challengeArg: PassportChallenge,
|
||||||
@@ -450,6 +548,7 @@ export class PassportManager {
|
|||||||
deviceLabel: optionsArg.deviceLabel,
|
deviceLabel: optionsArg.deviceLabel,
|
||||||
requireLocation: false,
|
requireLocation: false,
|
||||||
requireNfc: false,
|
requireNfc: false,
|
||||||
|
locationPolicy: undefined,
|
||||||
requestedCapabilities: this.normalizeCapabilities(optionsArg.capabilities),
|
requestedCapabilities: this.normalizeCapabilities(optionsArg.capabilities),
|
||||||
},
|
},
|
||||||
evidence: undefined,
|
evidence: undefined,
|
||||||
@@ -602,6 +701,7 @@ export class PassportManager {
|
|||||||
notificationTitle?: string;
|
notificationTitle?: string;
|
||||||
requireLocation?: boolean;
|
requireLocation?: boolean;
|
||||||
requireNfc?: boolean;
|
requireNfc?: boolean;
|
||||||
|
locationPolicy?: plugins.idpInterfaces.data.IPassportLocationPolicy;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const passportDevices = await this.getPassportDevicesForUser(userIdArg);
|
const passportDevices = await this.getPassportDevicesForUser(userIdArg);
|
||||||
@@ -631,8 +731,9 @@ export class PassportManager {
|
|||||||
audience: optionsArg.audience,
|
audience: optionsArg.audience,
|
||||||
notificationTitle: optionsArg.notificationTitle,
|
notificationTitle: optionsArg.notificationTitle,
|
||||||
deviceLabel: targetDevice.data.label,
|
deviceLabel: targetDevice.data.label,
|
||||||
requireLocation: !!optionsArg.requireLocation,
|
requireLocation: !!optionsArg.requireLocation || !!optionsArg.locationPolicy,
|
||||||
requireNfc: !!optionsArg.requireNfc,
|
requireNfc: !!optionsArg.requireNfc,
|
||||||
|
locationPolicy: optionsArg.locationPolicy,
|
||||||
},
|
},
|
||||||
evidence: undefined,
|
evidence: undefined,
|
||||||
notification: {
|
notification: {
|
||||||
@@ -716,9 +817,21 @@ export class PassportManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Passport signature invalid');
|
throw new plugins.typedrequest.TypedResponseError('Passport signature invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locationEvaluation =
|
||||||
|
passportChallenge.data.metadata.locationPolicy && optionsArg.location
|
||||||
|
? this.evaluateLocationPolicy(passportChallenge.data.metadata.locationPolicy, optionsArg.location)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (passportChallenge.data.metadata.locationPolicy && !locationEvaluation?.matched) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
locationEvaluation?.reason || 'Location evidence did not satisfy the office policy'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await passportChallenge.markApproved({
|
await passportChallenge.markApproved({
|
||||||
signatureFormat: optionsArg.signatureFormat || 'raw',
|
signatureFormat: optionsArg.signatureFormat || 'raw',
|
||||||
location: optionsArg.location,
|
location: optionsArg.location,
|
||||||
|
locationEvaluation,
|
||||||
nfc: optionsArg.nfc,
|
nfc: optionsArg.nfc,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -738,6 +851,36 @@ export class PassportManager {
|
|||||||
return passportChallenge;
|
return passportChallenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async rejectPassportChallenge(deviceIdArg: string, challengeIdArg: string) {
|
||||||
|
const passportChallenge = await this.CPassportChallenge.getInstance({
|
||||||
|
id: challengeIdArg,
|
||||||
|
'data.deviceId': deviceIdArg,
|
||||||
|
'data.status': 'pending',
|
||||||
|
});
|
||||||
|
if (!passportChallenge) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Passport challenge not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passportChallenge.isExpired()) {
|
||||||
|
await passportChallenge.markExpired();
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Passport challenge expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
await passportChallenge.markRejected();
|
||||||
|
|
||||||
|
await this.receptionRef.activityLogManager.logActivity(
|
||||||
|
passportChallenge.data.userId,
|
||||||
|
'passport_challenge_rejected',
|
||||||
|
`Rejected passport challenge ${passportChallenge.data.type}`,
|
||||||
|
{
|
||||||
|
targetId: passportChallenge.id,
|
||||||
|
targetType: 'passport-challenge',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return passportChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
public async listPendingChallengesForDevice(deviceIdArg: string) {
|
public async listPendingChallengesForDevice(deviceIdArg: string) {
|
||||||
const passportChallenges = await this.CPassportChallenge.getInstances({
|
const passportChallenges = await this.CPassportChallenge.getInstances({
|
||||||
'data.deviceId': deviceIdArg,
|
'data.deviceId': deviceIdArg,
|
||||||
|
|||||||
@@ -14,6 +14,36 @@ export class UserInvitationManager {
|
|||||||
|
|
||||||
public CUserInvitation = plugins.smartdata.setDefaultManagerForDoc(this, UserInvitation);
|
public CUserInvitation = plugins.smartdata.setDefaultManagerForDoc(this, UserInvitation);
|
||||||
|
|
||||||
|
private async emitOrganizationAlert(optionsArg: {
|
||||||
|
organizationId: string;
|
||||||
|
eventType: string;
|
||||||
|
severity: plugins.idpInterfaces.data.TAlertSeverity;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
actorUserId: string;
|
||||||
|
relatedEntityId?: string;
|
||||||
|
relatedEntityType?: string;
|
||||||
|
}) {
|
||||||
|
await this.receptionRef.alertManager.createAlertsForEvent({
|
||||||
|
category: 'admin',
|
||||||
|
organizationId: optionsArg.organizationId,
|
||||||
|
eventType: optionsArg.eventType,
|
||||||
|
severity: optionsArg.severity,
|
||||||
|
title: optionsArg.title,
|
||||||
|
body: optionsArg.body,
|
||||||
|
actorUserId: optionsArg.actorUserId,
|
||||||
|
relatedEntityId: optionsArg.relatedEntityId,
|
||||||
|
relatedEntityType: optionsArg.relatedEntityType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrganizationName(organizationIdArg: string) {
|
||||||
|
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
||||||
|
id: organizationIdArg,
|
||||||
|
});
|
||||||
|
return organization?.data.name || 'this organization';
|
||||||
|
}
|
||||||
|
|
||||||
constructor(receptionRefArg: Reception) {
|
constructor(receptionRefArg: Reception) {
|
||||||
this.receptionRef = receptionRefArg;
|
this.receptionRef = receptionRefArg;
|
||||||
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
@@ -88,6 +118,19 @@ export class UserInvitationManager {
|
|||||||
// Send invitation email
|
// Send invitation email
|
||||||
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
||||||
|
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_invitation_created',
|
||||||
|
severity: 'low',
|
||||||
|
title: 'Organization invitation created',
|
||||||
|
body: `${user.data.email} invited ${email} to ${await this.getOrganizationName(
|
||||||
|
requestArg.organizationId
|
||||||
|
)}.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: invitation.id,
|
||||||
|
relatedEntityType: 'invitation',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
invitation: await invitation.createSavableObject(),
|
invitation: await invitation.createSavableObject(),
|
||||||
@@ -189,6 +232,17 @@ export class UserInvitationManager {
|
|||||||
await invitation.regenerateToken();
|
await invitation.regenerateToken();
|
||||||
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
await this.sendInvitationEmail(invitation, requestArg.organizationId);
|
||||||
|
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_invitation_resent',
|
||||||
|
severity: 'low',
|
||||||
|
title: 'Organization invitation resent',
|
||||||
|
body: `${user.data.email} resent an invitation to ${invitation.data.email}.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: invitation.id,
|
||||||
|
relatedEntityType: 'invitation',
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true, message: 'Invitation resent.' };
|
return { success: true, message: 'Invitation resent.' };
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -231,10 +285,12 @@ export class UserInvitationManager {
|
|||||||
|
|
||||||
await role.delete();
|
await role.delete();
|
||||||
|
|
||||||
// Remove org from user's connectedOrgs
|
const removedUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
const memberUser = await this.receptionRef.userManager.CUser.getInstance({
|
|
||||||
id: requestArg.userId,
|
id: requestArg.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove org from user's connectedOrgs
|
||||||
|
const memberUser = removedUser;
|
||||||
if (memberUser && memberUser.data.connectedOrgs) {
|
if (memberUser && memberUser.data.connectedOrgs) {
|
||||||
memberUser.data.connectedOrgs = memberUser.data.connectedOrgs.filter(
|
memberUser.data.connectedOrgs = memberUser.data.connectedOrgs.filter(
|
||||||
orgId => orgId !== requestArg.organizationId
|
orgId => orgId !== requestArg.organizationId
|
||||||
@@ -242,6 +298,19 @@ export class UserInvitationManager {
|
|||||||
await memberUser.save();
|
await memberUser.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_member_removed',
|
||||||
|
severity: 'high',
|
||||||
|
title: 'Organization member removed',
|
||||||
|
body: `${user.data.email} removed ${removedUser?.data?.email || requestArg.userId} from ${await this.getOrganizationName(
|
||||||
|
requestArg.organizationId
|
||||||
|
)}.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: requestArg.userId,
|
||||||
|
relatedEntityType: 'user',
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -283,6 +352,20 @@ export class UserInvitationManager {
|
|||||||
role.data.roles = requestArg.roles;
|
role.data.roles = requestArg.roles;
|
||||||
await role.save();
|
await role.save();
|
||||||
|
|
||||||
|
const updatedUser = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: requestArg.userId,
|
||||||
|
});
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_member_roles_updated',
|
||||||
|
severity: 'high',
|
||||||
|
title: 'Organization member roles updated',
|
||||||
|
body: `${user.data.email} changed roles for ${updatedUser?.data?.email || requestArg.userId} to ${requestArg.roles.join(', ')}.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: requestArg.userId,
|
||||||
|
relatedEntityType: 'user',
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true, role: await role.createSavableObject() };
|
return { success: true, role: await role.createSavableObject() };
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -332,6 +415,20 @@ export class UserInvitationManager {
|
|||||||
}
|
}
|
||||||
await currentUserRole.save();
|
await currentUserRole.save();
|
||||||
|
|
||||||
|
const newOwner = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
|
id: requestArg.newOwnerId,
|
||||||
|
});
|
||||||
|
await this.emitOrganizationAlert({
|
||||||
|
organizationId: requestArg.organizationId,
|
||||||
|
eventType: 'org_ownership_transferred',
|
||||||
|
severity: 'critical',
|
||||||
|
title: 'Organization ownership transferred',
|
||||||
|
body: `${user.data.email} transferred ownership to ${newOwner?.data?.email || requestArg.newOwnerId}.`,
|
||||||
|
actorUserId: user.id,
|
||||||
|
relatedEntityId: requestArg.newOwnerId,
|
||||||
|
relatedEntityType: 'user',
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type TActivityAction =
|
|||||||
| 'passport_device_enrolled'
|
| 'passport_device_enrolled'
|
||||||
| 'passport_device_revoked'
|
| 'passport_device_revoked'
|
||||||
| 'passport_challenge_approved'
|
| 'passport_challenge_approved'
|
||||||
|
| 'passport_challenge_rejected'
|
||||||
| 'org_created'
|
| 'org_created'
|
||||||
| 'org_joined'
|
| 'org_joined'
|
||||||
| 'org_left'
|
| 'org_left'
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export interface IPassportNfcEvidence {
|
|||||||
readerId?: string;
|
readerId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPassportLocationPolicy {
|
||||||
|
mode: 'geofence';
|
||||||
|
label?: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
radiusMeters: number;
|
||||||
|
maxAccuracyMeters?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPassportChallenge {
|
export interface IPassportChallenge {
|
||||||
id: string;
|
id: string;
|
||||||
data: {
|
data: {
|
||||||
@@ -40,11 +49,19 @@ export interface IPassportChallenge {
|
|||||||
deviceLabel?: string;
|
deviceLabel?: string;
|
||||||
requireLocation: boolean;
|
requireLocation: boolean;
|
||||||
requireNfc: boolean;
|
requireNfc: boolean;
|
||||||
|
locationPolicy?: IPassportLocationPolicy;
|
||||||
requestedCapabilities?: Partial<IPassportCapabilities>;
|
requestedCapabilities?: Partial<IPassportCapabilities>;
|
||||||
};
|
};
|
||||||
evidence?: {
|
evidence?: {
|
||||||
signatureFormat?: TPassportSignatureFormat;
|
signatureFormat?: TPassportSignatureFormat;
|
||||||
location?: IPassportLocationEvidence;
|
location?: IPassportLocationEvidence;
|
||||||
|
locationEvaluation?: {
|
||||||
|
matched: boolean;
|
||||||
|
distanceMeters?: number;
|
||||||
|
accuracyAccepted?: boolean;
|
||||||
|
evaluatedAt: number;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
nfc?: IPassportNfcEvidence;
|
nfc?: IPassportNfcEvidence;
|
||||||
};
|
};
|
||||||
notification?: {
|
notification?: {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export interface IReq_ListPassportAlerts
|
|||||||
IReq_ListPassportAlerts
|
IReq_ListPassportAlerts
|
||||||
> {
|
> {
|
||||||
method: 'listPassportAlerts';
|
method: 'listPassportAlerts';
|
||||||
request: IPassportDeviceSignedRequest;
|
request: IPassportDeviceSignedRequest & {
|
||||||
|
includeDismissed?: boolean;
|
||||||
|
};
|
||||||
response: {
|
response: {
|
||||||
alerts: data.IAlert[];
|
alerts: data.IAlert[];
|
||||||
};
|
};
|
||||||
@@ -42,6 +44,20 @@ export interface IReq_MarkPassportAlertSeen
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReq_DismissPassportAlert
|
||||||
|
extends plugins.typedRequestInterfaces.implementsTR<
|
||||||
|
plugins.typedRequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DismissPassportAlert
|
||||||
|
> {
|
||||||
|
method: 'dismissPassportAlert';
|
||||||
|
request: IPassportDeviceSignedRequest & {
|
||||||
|
hintId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface IReq_UpsertAlertRule
|
export interface IReq_UpsertAlertRule
|
||||||
extends plugins.typedRequestInterfaces.implementsTR<
|
extends plugins.typedRequestInterfaces.implementsTR<
|
||||||
plugins.typedRequestInterfaces.ITypedRequest,
|
plugins.typedRequestInterfaces.ITypedRequest,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export interface IReq_CreatePassportChallenge
|
|||||||
notificationTitle?: string;
|
notificationTitle?: string;
|
||||||
requireLocation?: boolean;
|
requireLocation?: boolean;
|
||||||
requireNfc?: boolean;
|
requireNfc?: boolean;
|
||||||
|
locationPolicy?: data.IPassportLocationPolicy;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
challengeId: string;
|
challengeId: string;
|
||||||
@@ -125,6 +126,21 @@ export interface IReq_ApprovePassportChallenge
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReq_RejectPassportChallenge
|
||||||
|
extends plugins.typedRequestInterfaces.implementsTR<
|
||||||
|
plugins.typedRequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RejectPassportChallenge
|
||||||
|
> {
|
||||||
|
method: 'rejectPassportChallenge';
|
||||||
|
request: IPassportDeviceSignedRequest & {
|
||||||
|
challengeId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
challenge: data.IPassportChallenge;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface IReq_RegisterPassportPushToken
|
export interface IReq_RegisterPassportPushToken
|
||||||
extends plugins.typedRequestInterfaces.implementsTR<
|
extends plugins.typedRequestInterfaces.implementsTR<
|
||||||
plugins.typedRequestInterfaces.ITypedRequest,
|
plugins.typedRequestInterfaces.ITypedRequest,
|
||||||
@@ -164,7 +180,10 @@ export interface IReq_GetPassportChallengeByHint
|
|||||||
hintId: string;
|
hintId: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
challenge?: data.IPassportChallenge;
|
challenge?: {
|
||||||
|
challenge: data.IPassportChallenge;
|
||||||
|
signingPayload: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,3 +200,28 @@ export interface IReq_MarkPassportChallengeSeen
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetPassportDashboard
|
||||||
|
extends plugins.typedRequestInterfaces.implementsTR<
|
||||||
|
plugins.typedRequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetPassportDashboard
|
||||||
|
> {
|
||||||
|
method: 'getPassportDashboard';
|
||||||
|
request: IPassportDeviceSignedRequest;
|
||||||
|
response: {
|
||||||
|
profile: {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
handle: string;
|
||||||
|
organizations: Array<{ id: string; name: string }>;
|
||||||
|
deviceCount: number;
|
||||||
|
recoverySummary: string;
|
||||||
|
};
|
||||||
|
devices: data.IPassportDevice[];
|
||||||
|
challenges: Array<{
|
||||||
|
challenge: data.IPassportChallenge;
|
||||||
|
signingPayload: string;
|
||||||
|
}>;
|
||||||
|
alerts: data.IAlert[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user