feat: Update organization member management and bulk invite functionality

- Marked the status of "Invite and Manage Team Members" story as Complete in README.
- Updated the status of ORG-002 to Complete in the corresponding markdown file.
- Modified OrganizationManager to assign roles as 'owner' during organization creation.
- Implemented bulk invitation feature in UserInvitationManager, allowing multiple users to be invited via CSV upload.
- Added IReq_BulkCreateInvitations interface for bulk invitation requests.
- Enhanced CreateOrgForm to update state with new roles upon organization creation.
- Introduced BulkInviteModal for bulk inviting users, including email validation and role assignment.
- Updated UsersView to support ownership transfer and bulk invitation functionality.
- Improved account state management to handle new roles and organizations.
This commit is contained in:
2025-12-05 09:34:19 +00:00
parent 8df44b99b9
commit 833cf3b4b8
11 changed files with 1148 additions and 512 deletions
@@ -456,6 +456,159 @@ export class UserInvitationManager {
}
)
);
// Bulk create invitations
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_BulkCreateInvitations>(
'bulkCreateInvitations',
async (requestArg) => {
const user = await this.receptionRef.userManager.getUserByJwtValidation(requestArg.jwt);
await this.verifyUserIsAdminOfOrg(user.id, requestArg.organizationId);
const org = await this.receptionRef.organizationmanager.COrganization.getInstance({
id: requestArg.organizationId,
});
const orgName = org?.data.name || 'an organization';
const results: Array<{
email: string;
success: boolean;
status: 'invited' | 'already_member' | 'invalid_email' | 'error';
message?: string;
}> = [];
const summary = {
total: 0,
invited: 0,
alreadyMembers: 0,
invalid: 0,
errors: 0,
};
// Deduplicate emails in the batch
const processedEmails = new Set<string>();
for (const inv of requestArg.invitations) {
summary.total++;
const email = inv.email?.toLowerCase().trim();
// Validate email format
if (!email || !this.isValidEmail(email)) {
results.push({
email: inv.email || '',
success: false,
status: 'invalid_email',
message: 'Invalid email format',
});
summary.invalid++;
continue;
}
// Skip duplicates within batch
if (processedEmails.has(email)) {
results.push({
email,
success: false,
status: 'invalid_email',
message: 'Duplicate email in batch',
});
summary.invalid++;
continue;
}
processedEmails.add(email);
try {
// Check if user with this email already exists
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
data: { email },
});
if (existingUser) {
// Check if already a member
const existingRole = await this.receptionRef.roleManager.CRole.getInstance({
data: {
userId: existingUser.id,
organizationId: requestArg.organizationId,
},
});
if (existingRole) {
results.push({
email,
success: false,
status: 'already_member',
message: 'Already a member of this organization',
});
summary.alreadyMembers++;
continue;
}
// Add existing user to org
const roles = inv.roles?.length ? inv.roles : requestArg.defaultRoles;
await this.receptionRef.roleManager.modifyRoleForUserAtOrg({
action: 'create',
userId: existingUser.id,
organizationId: requestArg.organizationId,
roles,
});
results.push({
email,
success: true,
status: 'invited',
message: 'Existing user added to organization',
});
summary.invited++;
continue;
}
// Check if invitation already exists
let invitation = await this.CUserInvitation.getInstance({
data: { email },
});
const roles = inv.roles?.length ? inv.roles : requestArg.defaultRoles;
if (invitation) {
// Add org to existing invitation
await invitation.addOrganization(requestArg.organizationId, user.id, roles);
} else {
// Create new invitation
invitation = await UserInvitation.createNewInvitation(
email,
requestArg.organizationId,
user.id,
roles
);
}
// Send invitation email
await this.receptionRef.receptionMailer.sendInvitationEmail(
email,
orgName,
invitation.data.token,
this.receptionRef.options.baseUrl
);
results.push({
email,
success: true,
status: 'invited',
});
summary.invited++;
} catch (error: any) {
results.push({
email,
success: false,
status: 'error',
message: error.message || 'Unknown error',
});
summary.errors++;
}
}
return { success: true, results, summary };
}
)
);
}
/**
@@ -553,4 +706,12 @@ export class UserInvitationManager {
throw new plugins.typedrequest.TypedResponseError('Not a member of this organization.');
}
}
/**
* Validate email format
*/
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}