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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user