2024-10-01 13:49:18 +02:00
|
|
|
import * as plugins from '../plugins.js';
|
2024-09-29 13:56:38 +02:00
|
|
|
import { Reception } from './classes.reception.js';
|
|
|
|
|
import { Organization } from './classes.organization.js';
|
|
|
|
|
import { User } from './classes.user.js';
|
|
|
|
|
|
|
|
|
|
export class OrganizationManager {
|
2026-05-07 15:35:37 +00:00
|
|
|
public static readonly platformRoleKeys = ['owner', 'admin', 'editor', 'viewer', 'guest', 'outlaw'];
|
|
|
|
|
|
2024-09-29 13:56:38 +02:00
|
|
|
public receptionRef: Reception;
|
|
|
|
|
public get db() {
|
|
|
|
|
return this.receptionRef.db.smartdataDb;
|
|
|
|
|
}
|
|
|
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
|
|
|
|
|
|
|
|
public COrganization = plugins.smartdata.setDefaultManagerForDoc(this, Organization);
|
|
|
|
|
|
|
|
|
|
constructor(receptionRefArg: Reception) {
|
|
|
|
|
this.receptionRef = receptionRefArg;
|
|
|
|
|
this.receptionRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
|
|
|
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
2024-10-07 10:26:21 +02:00
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_CreateOrganization>(
|
2024-09-29 13:56:38 +02:00
|
|
|
'createOrganization',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const nameIsAvailable = async () => {
|
|
|
|
|
const existingOrg = await this.COrganization.getInstance({
|
|
|
|
|
data: {
|
|
|
|
|
slug: requestArg.organizationSlug,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const nameAvailable = !existingOrg;
|
|
|
|
|
return nameAvailable;
|
|
|
|
|
};
|
|
|
|
|
switch (requestArg.action) {
|
|
|
|
|
case 'checkAvailability':
|
|
|
|
|
return {
|
|
|
|
|
nameAvailable: await nameIsAvailable(),
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
case 'manifest':
|
|
|
|
|
const nameCheckedOk = await nameIsAvailable();
|
|
|
|
|
const userData = await this.receptionRef.userManager.getUserByJwtValidation(
|
|
|
|
|
requestArg.jwt
|
|
|
|
|
);
|
|
|
|
|
const newOrg = await this.COrganization.createNewOrganizationForUser(
|
|
|
|
|
this,
|
|
|
|
|
userData.id,
|
|
|
|
|
requestArg.organizationName,
|
|
|
|
|
requestArg.organizationSlug
|
|
|
|
|
);
|
|
|
|
|
const role = await this.receptionRef.roleManager.modifyRoleForUserAtOrg({
|
|
|
|
|
action: 'create',
|
|
|
|
|
organizationId: newOrg.id,
|
|
|
|
|
userId: userData.id,
|
2025-12-05 09:34:19 +00:00
|
|
|
roles: ['owner'],
|
2024-09-29 13:56:38 +02:00
|
|
|
});
|
|
|
|
|
newOrg.data.roleIds.push(role.id);
|
|
|
|
|
await newOrg.save();
|
|
|
|
|
return {
|
|
|
|
|
nameAvailable: true,
|
2025-12-05 09:34:19 +00:00
|
|
|
resultingOrganization: await newOrg.createSavableObject(),
|
|
|
|
|
role: await role.createSavableObject(),
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
2024-10-07 10:26:21 +02:00
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetOrganizationById>(
|
2024-09-29 13:56:38 +02:00
|
|
|
'getOrganizationById',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const verifiedJwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(
|
|
|
|
|
requestArg.jwt
|
|
|
|
|
);
|
|
|
|
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
|
|
|
|
id: verifiedJwt.data.userId,
|
|
|
|
|
});
|
|
|
|
|
const organization = await this.COrganization.getInstance({
|
|
|
|
|
id: requestArg.id
|
|
|
|
|
});
|
|
|
|
|
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 the requested organization.');
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
organization: await organization.createSavableObject()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-05-07 15:35:37 +00:00
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpdateOrganization>(
|
|
|
|
|
'updateOrganization',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
|
|
|
|
const organization = await this.updateOrganizationWithAudit({
|
|
|
|
|
user,
|
|
|
|
|
organizationId: requestArg.organizationId,
|
|
|
|
|
name: requestArg.name,
|
|
|
|
|
slug: requestArg.slug,
|
|
|
|
|
confirmationText: requestArg.confirmationText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
organization: await organization.createSavableObject(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_DeleteOrganization>(
|
|
|
|
|
'deleteOrganization',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
|
|
|
|
await this.deleteOrganizationWithAudit({
|
|
|
|
|
user,
|
|
|
|
|
organizationId: requestArg.organizationId,
|
|
|
|
|
confirmationText: requestArg.confirmationText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
deletedOrganizationId: requestArg.organizationId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetOrgRoleDefinitions>(
|
|
|
|
|
'getOrgRoleDefinitions',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(requestArg.organizationId);
|
|
|
|
|
await this.getRoleOrThrow(user, organization);
|
|
|
|
|
return {
|
|
|
|
|
roleDefinitions: this.getCustomRoleDefinitions(organization),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_UpsertOrgRoleDefinition>(
|
|
|
|
|
'upsertOrgRoleDefinition',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
|
|
|
|
const roleDefinitions = await this.upsertOrgRoleDefinition({
|
|
|
|
|
user,
|
|
|
|
|
organizationId: requestArg.organizationId,
|
|
|
|
|
roleDefinition: requestArg.roleDefinition,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
roleDefinitions,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_DeleteOrgRoleDefinition>(
|
|
|
|
|
'deleteOrgRoleDefinition',
|
|
|
|
|
async (requestArg) => {
|
|
|
|
|
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
|
|
|
|
|
const roleDefinitions = await this.deleteOrgRoleDefinition({
|
|
|
|
|
user,
|
|
|
|
|
organizationId: requestArg.organizationId,
|
|
|
|
|
roleKey: requestArg.roleKey,
|
|
|
|
|
confirmationText: requestArg.confirmationText,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
roleDefinitions,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getCustomRoleDefinitions(organizationArg: Organization) {
|
|
|
|
|
return organizationArg.data.roleDefinitions || [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private normalizeRoleKey(roleKeyArg: string) {
|
|
|
|
|
return (roleKeyArg || '').trim().toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public validateRoleKey(roleKeyArg: string) {
|
|
|
|
|
const roleKey = this.normalizeRoleKey(roleKeyArg);
|
|
|
|
|
if (!roleKey || roleKey.length < 2 || roleKey.length > 64) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Role key must be between 2 and 64 characters.');
|
|
|
|
|
}
|
|
|
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(roleKey)) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Role key may only contain lowercase letters, numbers, and single dashes.');
|
|
|
|
|
}
|
|
|
|
|
return roleKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getAvailableRoleKeys(organizationIdArg: string) {
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(organizationIdArg);
|
|
|
|
|
return [
|
|
|
|
|
...OrganizationManager.platformRoleKeys,
|
|
|
|
|
...this.getCustomRoleDefinitions(organization).map((roleDefinitionArg) => roleDefinitionArg.key),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async assertRoleKeysAreValid(organizationIdArg: string, roleKeysArg: string[]) {
|
|
|
|
|
const normalizedRoleKeys = [...new Set((roleKeysArg || []).map((roleKeyArg) => this.validateRoleKey(roleKeyArg)))];
|
|
|
|
|
if (!normalizedRoleKeys.length) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('At least one role is required.');
|
|
|
|
|
}
|
|
|
|
|
const availableRoleKeys = await this.getAvailableRoleKeys(organizationIdArg);
|
|
|
|
|
const invalidRoleKeys = normalizedRoleKeys.filter((roleKeyArg) => !availableRoleKeys.includes(roleKeyArg));
|
|
|
|
|
if (invalidRoleKeys.length) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError(`Unknown organization roles: ${invalidRoleKeys.join(', ')}.`);
|
|
|
|
|
}
|
|
|
|
|
return normalizedRoleKeys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private normalizeSlug(slugArg: string) {
|
|
|
|
|
return (slugArg || '').trim().toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private validateSlug(slugArg: string) {
|
|
|
|
|
const slug = this.normalizeSlug(slugArg);
|
|
|
|
|
if (!slug || slug.length < 3 || slug.length > 64) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization slug must be between 3 and 64 characters.');
|
|
|
|
|
}
|
|
|
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization slug may only contain lowercase letters, numbers, and single dashes.');
|
|
|
|
|
}
|
|
|
|
|
return slug;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private assertConfirmation(confirmationTextArg: string, expectedTextArg: string) {
|
|
|
|
|
if ((confirmationTextArg || '').trim() !== expectedTextArg) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError(`Confirmation text must be exactly "${expectedTextArg}".`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getOrganizationOrThrow(organizationIdArg: string) {
|
|
|
|
|
const organization = await this.COrganization.getInstance({
|
|
|
|
|
id: organizationIdArg,
|
|
|
|
|
});
|
|
|
|
|
if (!organization) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization not found.');
|
|
|
|
|
}
|
|
|
|
|
return organization;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getRoleOrThrow(userArg: User, organizationArg: Organization) {
|
|
|
|
|
const role = await this.receptionRef.roleManager.getRoleForUserAndOrg(userArg, organizationArg);
|
|
|
|
|
if (!role) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('User not authorized for this organization.');
|
|
|
|
|
}
|
|
|
|
|
return role;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async verifyAdmin(userArg: User, organizationArg: Organization) {
|
|
|
|
|
const role = await this.getRoleOrThrow(userArg, organizationArg);
|
|
|
|
|
if (!role.data.roles.some((roleArg) => ['owner', 'admin'].includes(roleArg))) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization admin privileges required.');
|
|
|
|
|
}
|
|
|
|
|
return role;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async verifyOwner(userArg: User, organizationArg: Organization) {
|
|
|
|
|
const role = await this.getRoleOrThrow(userArg, organizationArg);
|
|
|
|
|
if (!role.data.roles.includes('owner')) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization owner privileges required.');
|
|
|
|
|
}
|
|
|
|
|
return role;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async upsertOrgRoleDefinition(optionsArg: {
|
|
|
|
|
user: User;
|
|
|
|
|
organizationId: string;
|
|
|
|
|
roleDefinition: {
|
|
|
|
|
key: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
};
|
|
|
|
|
}) {
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(optionsArg.organizationId);
|
|
|
|
|
await this.verifyAdmin(optionsArg.user, organization);
|
|
|
|
|
const roleKey = this.validateRoleKey(optionsArg.roleDefinition.key);
|
|
|
|
|
if (OrganizationManager.platformRoleKeys.includes(roleKey)) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Platform roles cannot be redefined by an organization.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roleName = (optionsArg.roleDefinition.name || '').trim();
|
|
|
|
|
if (!roleName) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Role name is required.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const roleDefinitions = this.getCustomRoleDefinitions(organization);
|
|
|
|
|
const existingRoleDefinition = roleDefinitions.find((roleDefinitionArg) => roleDefinitionArg.key === roleKey);
|
|
|
|
|
if (existingRoleDefinition) {
|
|
|
|
|
existingRoleDefinition.name = roleName;
|
|
|
|
|
existingRoleDefinition.description = optionsArg.roleDefinition.description?.trim() || '';
|
|
|
|
|
existingRoleDefinition.updatedAt = now;
|
|
|
|
|
} else {
|
|
|
|
|
roleDefinitions.push({
|
|
|
|
|
key: roleKey,
|
|
|
|
|
name: roleName,
|
|
|
|
|
description: optionsArg.roleDefinition.description?.trim() || '',
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
organization.data.roleDefinitions = roleDefinitions.sort((leftArg, rightArg) => leftArg.name.localeCompare(rightArg.name));
|
|
|
|
|
await organization.save();
|
|
|
|
|
|
|
|
|
|
await this.receptionRef.activityLogManager.logActivity(
|
|
|
|
|
optionsArg.user.id,
|
|
|
|
|
'role_changed',
|
|
|
|
|
`${optionsArg.user.data.email} ${existingRoleDefinition ? 'updated' : 'created'} organization role ${roleKey}.`,
|
|
|
|
|
{
|
|
|
|
|
targetId: organization.id,
|
|
|
|
|
targetType: 'organization-role',
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.emitOrganizationAlert({
|
|
|
|
|
organizationId: organization.id,
|
|
|
|
|
eventType: 'org_role_definition_updated',
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
title: 'Organization role definition updated',
|
|
|
|
|
body: `${optionsArg.user.data.email} ${existingRoleDefinition ? 'updated' : 'created'} organization role ${roleKey}.`,
|
|
|
|
|
actorUserId: optionsArg.user.id,
|
|
|
|
|
relatedEntityId: roleKey,
|
|
|
|
|
relatedEntityType: 'organization-role',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return organization.data.roleDefinitions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async deleteOrgRoleDefinition(optionsArg: {
|
|
|
|
|
user: User;
|
|
|
|
|
organizationId: string;
|
|
|
|
|
roleKey: string;
|
|
|
|
|
confirmationText: string;
|
|
|
|
|
}) {
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(optionsArg.organizationId);
|
|
|
|
|
await this.verifyAdmin(optionsArg.user, organization);
|
|
|
|
|
const roleKey = this.validateRoleKey(optionsArg.roleKey);
|
|
|
|
|
if (OrganizationManager.platformRoleKeys.includes(roleKey)) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Platform roles cannot be deleted.');
|
|
|
|
|
}
|
|
|
|
|
this.assertConfirmation(optionsArg.confirmationText, `delete role ${roleKey}`);
|
|
|
|
|
|
|
|
|
|
const roleDefinitions = this.getCustomRoleDefinitions(organization);
|
|
|
|
|
if (!roleDefinitions.some((roleDefinitionArg) => roleDefinitionArg.key === roleKey)) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization role definition not found.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
organization.data.roleDefinitions = roleDefinitions.filter((roleDefinitionArg) => roleDefinitionArg.key !== roleKey);
|
|
|
|
|
await organization.save();
|
|
|
|
|
|
|
|
|
|
const roles = await this.receptionRef.roleManager.getAllRolesForOrg(organization.id);
|
|
|
|
|
for (const role of roles) {
|
|
|
|
|
if (role.data.roles.includes(roleKey)) {
|
|
|
|
|
role.data.roles = role.data.roles.filter((roleKeyArg) => roleKeyArg !== roleKey);
|
|
|
|
|
if (!role.data.roles.length) {
|
|
|
|
|
role.data.roles = ['viewer'];
|
|
|
|
|
}
|
|
|
|
|
await role.save();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const appConnections = await this.receptionRef.appConnectionManager.CAppConnection.getInstances({
|
|
|
|
|
'data.organizationId': organization.id,
|
|
|
|
|
});
|
|
|
|
|
for (const connection of appConnections) {
|
|
|
|
|
if (connection.data.roleMappings?.some((mappingArg) => mappingArg.orgRoleKey === roleKey)) {
|
|
|
|
|
connection.data.roleMappings = connection.data.roleMappings.filter((mappingArg) => mappingArg.orgRoleKey !== roleKey);
|
|
|
|
|
await connection.save();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.receptionRef.activityLogManager.logActivity(
|
|
|
|
|
optionsArg.user.id,
|
|
|
|
|
'role_changed',
|
|
|
|
|
`${optionsArg.user.data.email} deleted organization role ${roleKey}.`,
|
|
|
|
|
{
|
|
|
|
|
targetId: organization.id,
|
|
|
|
|
targetType: 'organization-role',
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.emitOrganizationAlert({
|
|
|
|
|
organizationId: organization.id,
|
|
|
|
|
eventType: 'org_role_definition_deleted',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Organization role definition deleted',
|
|
|
|
|
body: `${optionsArg.user.data.email} deleted organization role ${roleKey}. Member assignments and app mappings were cleaned up.`,
|
|
|
|
|
actorUserId: optionsArg.user.id,
|
|
|
|
|
relatedEntityId: roleKey,
|
|
|
|
|
relatedEntityType: 'organization-role',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return organization.data.roleDefinitions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async updateOrganizationWithAudit(optionsArg: {
|
|
|
|
|
user: User;
|
|
|
|
|
organizationId: string;
|
|
|
|
|
name?: string;
|
|
|
|
|
slug?: string;
|
|
|
|
|
confirmationText: string;
|
|
|
|
|
}) {
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(optionsArg.organizationId);
|
|
|
|
|
await this.verifyAdmin(optionsArg.user, organization);
|
|
|
|
|
this.assertConfirmation(optionsArg.confirmationText, organization.data.slug);
|
|
|
|
|
|
|
|
|
|
const previousName = organization.data.name;
|
|
|
|
|
const previousSlug = organization.data.slug;
|
|
|
|
|
const nextName = typeof optionsArg.name === 'string' ? optionsArg.name.trim() : previousName;
|
|
|
|
|
const nextSlug = typeof optionsArg.slug === 'string' ? this.validateSlug(optionsArg.slug) : previousSlug;
|
|
|
|
|
|
|
|
|
|
if (!nextName) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization name is required.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextSlug !== previousSlug) {
|
|
|
|
|
const existingOrganization = await this.COrganization.getInstance({
|
|
|
|
|
data: {
|
|
|
|
|
slug: nextSlug,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (existingOrganization && existingOrganization.id !== organization.id) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Organization slug is already in use.');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
organization.data.name = nextName;
|
|
|
|
|
organization.data.slug = nextSlug;
|
|
|
|
|
await organization.save();
|
|
|
|
|
|
|
|
|
|
const changes = [
|
|
|
|
|
previousName !== nextName ? `name "${previousName}" -> "${nextName}"` : '',
|
|
|
|
|
previousSlug !== nextSlug ? `slug "${previousSlug}" -> "${nextSlug}"` : '',
|
|
|
|
|
].filter(Boolean).join(', ') || 'no field changes';
|
|
|
|
|
|
|
|
|
|
await this.receptionRef.activityLogManager.logActivity(
|
|
|
|
|
optionsArg.user.id,
|
|
|
|
|
'org_updated',
|
|
|
|
|
`Organization ${previousName} updated: ${changes}.`,
|
|
|
|
|
{
|
|
|
|
|
targetId: organization.id,
|
|
|
|
|
targetType: 'organization',
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.emitOrganizationAlert({
|
|
|
|
|
organizationId: organization.id,
|
|
|
|
|
eventType: 'org_updated',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Organization settings updated',
|
|
|
|
|
body: `${optionsArg.user.data.email} updated ${previousName}: ${changes}.`,
|
|
|
|
|
actorUserId: optionsArg.user.id,
|
|
|
|
|
relatedEntityId: organization.id,
|
|
|
|
|
relatedEntityType: 'organization',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return organization;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async deleteOrganizationWithAudit(optionsArg: {
|
|
|
|
|
user: User;
|
|
|
|
|
organizationId: string;
|
|
|
|
|
confirmationText: string;
|
|
|
|
|
}) {
|
|
|
|
|
const organization = await this.getOrganizationOrThrow(optionsArg.organizationId);
|
|
|
|
|
await this.verifyOwner(optionsArg.user, organization);
|
|
|
|
|
this.assertConfirmation(optionsArg.confirmationText, `delete ${organization.data.slug}`);
|
|
|
|
|
|
|
|
|
|
const organizationName = organization.data.name;
|
|
|
|
|
const organizationSlug = organization.data.slug;
|
|
|
|
|
const roles = await this.receptionRef.roleManager.getAllRolesForOrg(organization.id);
|
|
|
|
|
const appConnections = await this.receptionRef.appConnectionManager.CAppConnection.getInstances({
|
|
|
|
|
'data.organizationId': organization.id,
|
|
|
|
|
});
|
|
|
|
|
const invitations = await this.receptionRef.userInvitationManager.CUserInvitation.getInstances({});
|
|
|
|
|
const billingPlans = await this.receptionRef.billingPlanManager.CBillingPlan.getInstances({
|
|
|
|
|
'data.organizationId': organization.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.receptionRef.activityLogManager.logActivity(
|
|
|
|
|
optionsArg.user.id,
|
|
|
|
|
'org_deleted',
|
|
|
|
|
`Organization ${organizationName} (${organizationSlug}) deleted.`,
|
|
|
|
|
{
|
|
|
|
|
targetId: organization.id,
|
|
|
|
|
targetType: 'organization',
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.emitOrganizationAlert({
|
|
|
|
|
organizationId: organization.id,
|
|
|
|
|
eventType: 'org_deleted',
|
|
|
|
|
severity: 'critical',
|
|
|
|
|
title: 'Organization deleted',
|
|
|
|
|
body: `${optionsArg.user.data.email} deleted ${organizationName}. ${roles.length} memberships and ${appConnections.length} app connections were removed.`,
|
|
|
|
|
actorUserId: optionsArg.user.id,
|
|
|
|
|
relatedEntityId: organization.id,
|
|
|
|
|
relatedEntityType: 'organization',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const connection of appConnections) {
|
|
|
|
|
await connection.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const invitation of invitations) {
|
|
|
|
|
if (invitation.data.organizationRefs.some((refArg) => refArg.organizationId === organization.id)) {
|
|
|
|
|
await invitation.removeOrganization(organization.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const billingPlan of billingPlans) {
|
|
|
|
|
await billingPlan.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const role of roles) {
|
|
|
|
|
const memberUser = await this.receptionRef.userManager.CUser.getInstance({
|
|
|
|
|
id: role.data.userId,
|
|
|
|
|
});
|
|
|
|
|
if (memberUser?.data.connectedOrgs) {
|
|
|
|
|
memberUser.data.connectedOrgs = memberUser.data.connectedOrgs.filter(
|
|
|
|
|
(organizationIdArg) => organizationIdArg !== organization.id
|
|
|
|
|
);
|
|
|
|
|
await memberUser.save();
|
|
|
|
|
}
|
|
|
|
|
await role.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await organization.delete();
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getAllOrganizationsForUser(
|
|
|
|
|
userArg: User,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const organizations: Organization[] = [];
|
|
|
|
|
const userRoles = await this.receptionRef.roleManager.getAllRolesForUser(userArg);
|
|
|
|
|
|
|
|
|
|
for (const role of userRoles) {
|
|
|
|
|
const organization = await this.receptionRef.organizationmanager.COrganization.getInstance({
|
|
|
|
|
id: role.data.organizationId
|
|
|
|
|
});
|
|
|
|
|
if (!organizations.find(orgArg => orgArg.id === organization.id)) {
|
|
|
|
|
organizations.push(organization);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return organizations;
|
|
|
|
|
}
|
|
|
|
|
}
|