import * as plugins from '../plugins.js'; import type { Reception } from './classes.reception.js'; import { AppConnection } from './classes.appconnection.js'; import type { User } from './classes.user.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( '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( '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 || [], roleMappings: [], }; 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(), }; } } ) ); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'updateAppRoleMappings', async (requestArg) => { const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt); const user = await this.receptionRef.userManager.CUser.getInstance({ id: jwtData.data.userId, }); const connection = await this.updateAppRoleMappings({ user, organizationId: requestArg.organizationId, appId: requestArg.appId, roleMappings: requestArg.roleMappings, }); return { success: true, connection: await connection.createSavableObject(), }; } ) ); } public async updateAppRoleMappings(optionsArg: { user: User; organizationId: string; appId: string; roleMappings: plugins.idpInterfaces.data.IAppRoleMapping[]; }) { const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({ id: optionsArg.organizationId, }); if (!organization) { throw new plugins.typedrequest.TypedResponseError('Organization not found'); } if (!await organization.checkIfUserIsAdmin(optionsArg.user)) { throw new plugins.typedrequest.TypedResponseError('Only organization admins can manage app role mappings'); } const app = await this.receptionRef.appManager.getAppById(optionsArg.appId); if (!app) { throw new plugins.typedrequest.TypedResponseError('App not found'); } const connection = await this.CAppConnection.getInstance({ 'data.organizationId': optionsArg.organizationId, 'data.appId': optionsArg.appId, }); if (!connection || !connection.isActive()) { throw new plugins.typedrequest.TypedResponseError('App must be connected before role mappings can be configured'); } const availableRoleKeys = await this.receptionRef.organizationmanager.getAvailableRoleKeys(optionsArg.organizationId); const cleanMappings = (optionsArg.roleMappings || []).map((mappingArg) => ({ orgRoleKey: this.receptionRef.organizationmanager.validateRoleKey(mappingArg.orgRoleKey), appRoles: this.cleanStringList(mappingArg.appRoles), permissions: this.cleanStringList(mappingArg.permissions), scopes: this.cleanStringList(mappingArg.scopes), })).filter((mappingArg) => mappingArg.appRoles.length || mappingArg.permissions.length || mappingArg.scopes.length); const invalidRoleKeys = cleanMappings .map((mappingArg) => mappingArg.orgRoleKey) .filter((roleKeyArg) => !availableRoleKeys.includes(roleKeyArg)); if (invalidRoleKeys.length) { throw new plugins.typedrequest.TypedResponseError(`Unknown organization roles: ${[...new Set(invalidRoleKeys)].join(', ')}.`); } const requestedScopes = cleanMappings.flatMap((mappingArg) => mappingArg.scopes); const allowedScopes = app.data.oauthCredentials?.allowedScopes || []; const grantedScopes = connection.data.grantedScopes || []; const unsupportedScopes = requestedScopes.filter((scopeArg) => !allowedScopes.includes(scopeArg)); if (unsupportedScopes.length) { throw new plugins.typedrequest.TypedResponseError(`Unsupported app scopes: ${[...new Set(unsupportedScopes)].join(', ')}.`); } const ungrantedScopes = requestedScopes.filter((scopeArg) => !grantedScopes.includes(scopeArg)); if (ungrantedScopes.length) { throw new plugins.typedrequest.TypedResponseError(`Scopes not granted to this connection: ${[...new Set(ungrantedScopes)].join(', ')}.`); } connection.data.roleMappings = cleanMappings; await connection.save(); await this.receptionRef.activityLogManager.logActivity( optionsArg.user.id, 'org_app_role_mappings_updated', `${optionsArg.user.data.email} updated ${cleanMappings.length} role mappings for ${app.data.name}.`, { targetId: connection.id, targetType: 'app-connection', } ); await this.emitOrganizationAlert({ organizationId: optionsArg.organizationId, eventType: 'org_app_role_mappings_updated', severity: 'medium', title: 'Organization app role mappings updated', body: `${optionsArg.user.data.email} updated role mappings for ${app.data.name}.`, actorUserId: optionsArg.user.id, relatedEntityId: app.id, relatedEntityType: 'global-app', }); return connection; } private cleanStringList(valuesArg: string[]) { return [...new Set((valuesArg || []) .map((valueArg) => (valueArg || '').trim()) .filter(Boolean))]; } /** * Get all connections for an organization */ public async getConnectionsForOrganization(organizationId: string): Promise { return await this.CAppConnection.getInstances({ 'data.organizationId': organizationId, }); } /** * Get connection for a specific app and organization */ public async getConnection( organizationId: string, appId: string ): Promise { 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 { const connection = await this.getConnection(organizationId, appId); return connection?.isActive() || false; } }