e9eb9b4172
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.
233 lines
7.8 KiB
TypeScript
233 lines
7.8 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import type { Reception } from './classes.reception.js';
|
|
import { AppConnection } from './classes.appconnection.js';
|
|
|
|
export class AppConnectionManager {
|
|
public receptionRef: Reception;
|
|
public get db() {
|
|
return this.receptionRef.db.smartdataDb;
|
|
}
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
|
|
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) {
|
|
this.receptionRef = receptionRefArg;
|
|
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
|
|
// Handler: Get app connections for an organization
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetAppConnections>(
|
|
'getAppConnections',
|
|
async (requestArg) => {
|
|
// Verify JWT and get user
|
|
const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
|
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
|
id: jwtData.data.userId,
|
|
});
|
|
|
|
// Check user has access to the organization
|
|
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
|
id: requestArg.organizationId,
|
|
});
|
|
|
|
if (!organization) {
|
|
throw new plugins.typedrequest.TypedResponseError('Organization not found');
|
|
}
|
|
|
|
const role = await this.receptionRef.roleManager.CRole.getInstance({
|
|
data: {
|
|
organizationId: organization.id,
|
|
userId: user.id,
|
|
},
|
|
});
|
|
|
|
if (!role) {
|
|
throw new plugins.typedrequest.TypedResponseError(
|
|
'User not authorized for this organization'
|
|
);
|
|
}
|
|
|
|
// Get all connections for this organization
|
|
const connections = await this.CAppConnection.getInstances({
|
|
'data.organizationId': requestArg.organizationId,
|
|
});
|
|
|
|
const connectionObjects = await Promise.all(
|
|
connections.map(async (conn) => await conn.createSavableObject())
|
|
);
|
|
|
|
return {
|
|
connections: connectionObjects,
|
|
};
|
|
}
|
|
)
|
|
);
|
|
|
|
// Handler: Toggle app connection (connect/disconnect)
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ToggleAppConnection>(
|
|
'toggleAppConnection',
|
|
async (requestArg) => {
|
|
// Verify JWT and get user
|
|
const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
|
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
|
id: jwtData.data.userId,
|
|
});
|
|
|
|
// Check user has admin access to the organization
|
|
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
|
id: requestArg.organizationId,
|
|
});
|
|
|
|
if (!organization) {
|
|
throw new plugins.typedrequest.TypedResponseError('Organization not found');
|
|
}
|
|
|
|
const isAdmin = await organization.checkIfUserIsAdmin(user);
|
|
if (!isAdmin) {
|
|
throw new plugins.typedrequest.TypedResponseError(
|
|
'Only organization admins can manage app connections'
|
|
);
|
|
}
|
|
|
|
// Get the app
|
|
const app = await this.receptionRef.appManager.getAppById(requestArg.appId);
|
|
if (!app) {
|
|
throw new plugins.typedrequest.TypedResponseError('App not found');
|
|
}
|
|
|
|
// Find existing connection
|
|
let connection = await this.CAppConnection.getInstance({
|
|
'data.organizationId': requestArg.organizationId,
|
|
'data.appId': requestArg.appId,
|
|
});
|
|
|
|
if (requestArg.action === 'connect') {
|
|
if (connection && connection.isActive()) {
|
|
// Already connected
|
|
return {
|
|
success: true,
|
|
connection: await connection.createSavableObject(),
|
|
};
|
|
}
|
|
|
|
if (connection) {
|
|
// Reconnect existing connection
|
|
await connection.reconnect(user.id);
|
|
} else {
|
|
// Create new connection
|
|
connection = new AppConnection();
|
|
connection.id = plugins.smartunique.shortId();
|
|
connection.data = {
|
|
organizationId: requestArg.organizationId,
|
|
appId: requestArg.appId,
|
|
appType: app.type,
|
|
status: 'active',
|
|
connectedAt: Date.now(),
|
|
connectedByUserId: user.id,
|
|
grantedScopes: app.data.oauthCredentials?.allowedScopes || [],
|
|
};
|
|
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 {
|
|
success: true,
|
|
connection: await connection.createSavableObject(),
|
|
};
|
|
} else {
|
|
// Disconnect
|
|
if (!connection) {
|
|
return {
|
|
success: true,
|
|
};
|
|
}
|
|
|
|
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 {
|
|
success: true,
|
|
connection: await connection.createSavableObject(),
|
|
};
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all connections for an organization
|
|
*/
|
|
public async getConnectionsForOrganization(organizationId: string): Promise<AppConnection[]> {
|
|
return await this.CAppConnection.getInstances({
|
|
'data.organizationId': organizationId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get connection for a specific app and organization
|
|
*/
|
|
public async getConnection(
|
|
organizationId: string,
|
|
appId: string
|
|
): Promise<AppConnection | null> {
|
|
return await this.CAppConnection.getInstance({
|
|
'data.organizationId': organizationId,
|
|
'data.appId': appId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if an app is connected to an organization
|
|
*/
|
|
public async isAppConnected(organizationId: string, appId: string): Promise<boolean> {
|
|
const connection = await this.getConnection(organizationId, appId);
|
|
return connection?.isActive() || false;
|
|
}
|
|
}
|