feat(app): wire dashboard administration flows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -150,6 +151,7 @@ export class AppConnectionManager {
|
||||
connectedAt: Date.now(),
|
||||
connectedByUserId: user.id,
|
||||
grantedScopes: app.data.oauthCredentials?.allowedScopes || [],
|
||||
roleMappings: [],
|
||||
};
|
||||
await connection.save();
|
||||
}
|
||||
@@ -198,6 +200,116 @@ export class AppConnectionManager {
|
||||
}
|
||||
)
|
||||
);
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpdateAppRoleMappings>(
|
||||
'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))];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user