Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: send email for delete organization invitations #798

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ export class OrganizationController {
@ApiOperation({ summary: 'Delete Organization', description: 'Delete an organization' })
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
@Roles(OrgRoles.OWNER)
async deleteOrganization(
@Param('orgId', TrimStringParamPipe, new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string,
Expand Down
13 changes: 12 additions & 1 deletion apps/organization/interfaces/organization.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,15 @@ export interface ILedgerDetails {
indyNamespace: string;
networkUrl: string;

}
}

export interface IOrgRoleDetails {
id: string;
name: string;
description: string;
createDateTime: Date;
createdBy: string;
lastChangedDateTime: Date;
lastChangedBy: string;
deletedAt: Date;
}
48 changes: 44 additions & 4 deletions apps/organization/repositories/organization.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ConflictException, Injectable, Logger, NotFoundException } from '@nestj
import { Prisma, agent_invitations, org_agents, org_invitations, user, user_org_roles } from '@prisma/client';

import { CreateOrganizationDto } from '../dtos/create-organization.dto';
import { IGetDids, IDidDetails, IDidList, IGetOrgById, IGetOrganization, IPrimaryDidDetails, IUpdateOrganization, ILedgerNameSpace, OrgInvitation, ILedgerDetails } from '../interfaces/organization.interface';
import { IGetDids, IDidDetails, IDidList, IGetOrgById, IGetOrganization, IPrimaryDidDetails, IUpdateOrganization, ILedgerNameSpace, OrgInvitation, ILedgerDetails, IOrgRoleDetails } from '../interfaces/organization.interface';
import { InternalServerErrorException } from '@nestjs/common';
import { Invitation, PrismaTables, SortValue } from '@credebl/enum/enum';
import { PrismaService } from '@credebl/prisma-service';
Expand Down Expand Up @@ -764,6 +764,7 @@ export class OrganizationRepository {
deletedUserActivity: Prisma.BatchPayload;
deletedUserOrgRole: Prisma.BatchPayload;
deletedOrgInvitations: Prisma.BatchPayload;
deletedNotification: Prisma.BatchPayload;
deleteOrg: IDeleteOrganization
}> {
const tablesToCheck = [
Expand All @@ -775,8 +776,7 @@ export class OrganizationRepository {
`${PrismaTables.PRESENTATIONS}`,
`${PrismaTables.ECOSYSTEM_INVITATIONS}`,
`${PrismaTables.ECOSYSTEM_ORGS}`,
`${PrismaTables.FILE_UPLOAD}`,
`${PrismaTables.NOTIFICATION}`
`${PrismaTables.FILE_UPLOAD}`
];

try {
Expand Down Expand Up @@ -806,6 +806,8 @@ export class OrganizationRepository {
throw new ConflictException(ResponseMessages.organisation.error.organizationEcosystemValidate);
}

const deletedNotification = await prisma.notification.deleteMany({ where: { orgId: id } });

const deletedUserActivity = await prisma.user_activity.deleteMany({ where: { orgId: id } });

const deletedUserOrgRole = await prisma.user_org_roles.deleteMany({ where: { orgId: id } });
Expand All @@ -825,8 +827,9 @@ export class OrganizationRepository {
// If no references are found, delete the organization
const deleteOrg = await prisma.organisation.delete({ where: { id } });

return {deletedUserActivity, deletedUserOrgRole, deletedOrgInvitations, deleteOrg};
return {deletedUserActivity, deletedUserOrgRole, deletedOrgInvitations, deletedNotification, deleteOrg};
});
// return result;
} catch (error) {
this.logger.error(`Error in deleteOrg: ${error}`);
throw error;
Expand Down Expand Up @@ -997,4 +1000,41 @@ async getDidDetailsByDid(did:string): Promise<IDidDetails> {
throw error;
}
}

async getOrgRole(id: string[]): Promise<IOrgRoleDetails[]> {
try {
const orgRoleData = await this.prisma.org_roles.findMany({
where: {
id: {
in: id
}
}
});
return orgRoleData;
} catch (error) {
this.logger.error(`[getOrgRole] - get org role details: ${JSON.stringify(error)}`);
throw error;
}
}

async getUserOrgRole(userId: string, orgId: string): Promise<string[]> {
try {
const userOrgRoleDetails = await this.prisma.user_org_roles.findMany({
where: {
userId,
orgId
},
select:{
orgRoleId: true
}
});
// Map the result to an array of orgRoleId
const orgRoleIds = userOrgRoleDetails.map(role => role.orgRoleId);

return orgRoleIds;
} catch (error) {
this.logger.error(`[getUserOrgRole] - get user org role details: ${JSON.stringify(error)}`);
throw error;
}
}
}
89 changes: 74 additions & 15 deletions apps/organization/src/organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { sendEmail } from '@credebl/common/send-grid-helper-file';
import { CreateOrganizationDto } from '../dtos/create-organization.dto';
import { BulkSendInvitationDto } from '../dtos/send-invitation.dto';
import { UpdateInvitationDto } from '../dtos/update-invitation.dt';
import { DidMethod, Invitation, Ledgers, transition } from '@credebl/enum/enum';
import { DidMethod, Invitation, Ledgers, PrismaTables, transition } from '@credebl/enum/enum';
import { IGetOrgById, IGetOrganization, IUpdateOrganization, IOrgAgent, IClientCredentials, ICreateConnectionUrl, IOrgRole, IDidList, IPrimaryDidDetails } from '../interfaces/organization.interface';
import { UserActivityService } from '@credebl/user-activity';
import { ClientRegistrationService } from '@credebl/client-registration/client-registration.service';
Expand All @@ -48,6 +48,7 @@ import { IAccessTokenData } from '@credebl/common/interfaces/interface';
import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface';
import { toNumber } from '@credebl/common/cast.helper';
import { UserActivityRepository } from 'libs/user-activity/repositories';
import { DeleteOrgInvitationsEmail } from '../templates/delete-organization-invitations.template';
@Injectable()
export class OrganizationService {
constructor(
Expand Down Expand Up @@ -1489,21 +1490,22 @@ export class OrganizationService {
}

const organizationInvitationDetails = await this.organizationRepository.getOrgInvitationsByOrg(orgId);
const userEmails = organizationInvitationDetails.map(userData => userData?.email);

this.logger.debug(`userEmails ::: ${JSON.stringify(userEmails)}`);

const arrayEmail = organizationInvitationDetails.map(userData => userData.email);
this.logger.debug(`arrayEmail ::: ${JSON.stringify(arrayEmail)}`);

// Fetch Keycloak IDs only if there are emails to process
const keycloakUserIds = 0 < userEmails.length
? (await this.getUserKeycloakIdByEmail(userEmails)).response
const keycloakUserIds = 0 < arrayEmail.length
? (await this.getUserKeycloakIdByEmail(arrayEmail)).response.map(user => user.keycloakUserId)
: [];

this.logger.log('Keycloak User Ids');

// Delete user client roles in parallel
const deleteUserRolesPromises = keycloakUserIds.map(keycloakUserId => this.clientRegistrationService.deleteUserClientRoles(organizationDetails?.idpId, token, keycloakUserId)
);
deleteUserRolesPromises.push(
this.clientRegistrationService.deleteUserClientRoles(organizationDetails?.idpId, token, user?.keycloakUserId)
this.clientRegistrationService.deleteUserClientRoles(organizationDetails?.idpId, token, getUser?.keycloakUserId)
);

this.logger.debug(`deleteUserRolesPromises ::: ${JSON.stringify(deleteUserRolesPromises)}`);
Expand All @@ -1518,28 +1520,64 @@ export class OrganizationService {
throw new NotFoundException(ResponseMessages.organisation.error.orgDataNotFoundInkeycloak);
}

const deletedOrgInvitationInfo: { email?: string, orgName?: string, orgRoleNames?: string[] }[] = [];
const userIds = (await this.getUserKeycloakIdByEmail(arrayEmail)).response.map(user => user.id);
await Promise.all(userIds.map(async (userId) => {
const userOrgRoleIds = await this.organizationRepository.getUserOrgRole(userId, orgId);
this.logger.debug(`userOrgRoleIds ::::: ${JSON.stringify(userOrgRoleIds)}`);

const userDetails = await this.organizationRepository.getUser(userId);
this.logger.debug(`userDetails ::::: ${JSON.stringify(userDetails)}`);

const orgRoles = await this.organizationRepository.getOrgRole(userOrgRoleIds);
this.logger.debug(`orgRoles ::::: ${JSON.stringify(orgRoles)}`);

const orgRoleNames = orgRoles.map(orgRoleName => orgRoleName.name);
const sendEmail = await this.sendEmailForOrgInvitationsMember(userDetails?.email, organizationDetails?.name, orgRoleNames);
const newInvitation = {
email: userDetails.email,
orgName: organizationDetails?.name,
orgRoleNames
};

// Step 3: Push the data into the array
deletedOrgInvitationInfo.push(newInvitation);

this.logger.log(`email: ${userDetails.email}, orgName: ${organizationDetails?.name}, orgRoles: ${JSON.stringify(orgRoleNames)}, sendEmail: ${sendEmail}`);
}));

// Delete organization data
const { deletedUserActivity, deletedUserOrgRole, deleteOrg, deletedOrgInvitations } = await this.organizationRepository.deleteOrg(orgId);
const { deletedUserActivity, deletedUserOrgRole, deleteOrg, deletedOrgInvitations, deletedNotification } = await this.organizationRepository.deleteOrg(orgId);

this.logger.debug(`deletedUserActivity ::: ${JSON.stringify(deletedUserActivity)}`);
this.logger.debug(`deletedUserOrgRole ::: ${JSON.stringify(deletedUserOrgRole)}`);
this.logger.debug(`deleteOrg ::: ${JSON.stringify(deleteOrg)}`);
this.logger.debug(`deletedOrgInvitations ::: ${JSON.stringify(deletedOrgInvitations)}`);

const deletions = [
{ records: deletedUserActivity.count, tableName: 'user_activity' },
{ records: deletedUserOrgRole.count, tableName: 'user_org_roles' },
{ records: deletedOrgInvitations.count, tableName: 'org_invitations' },
{ records: deleteOrg ? 1 : 0, tableName: 'organization' }
{ records: deletedUserActivity.count, tableName: `${PrismaTables.USER_ACTIVITY}` },
{ records: deletedUserOrgRole.count, tableName: `${PrismaTables.USER_ORG_ROLES}` },
{ records: deletedOrgInvitations.count, deletedOrgInvitationInfo, tableName: `${PrismaTables.ORG_INVITATIONS}` },
{ records: deletedNotification.count, tableName: `${PrismaTables.NOTIFICATION}` },
{ records: deleteOrg ? 1 : 0, tableName: `${PrismaTables.ORGANIZATION}` }
];

// Log deletion activities in parallel
await Promise.all(deletions.map(async ({ records, tableName }) => {
await Promise.all(deletions.map(async ({ records, tableName, deletedOrgInvitationInfo }) => {
if (records) {
const txnMetadata = {
const txnMetadata: {
deletedRecordsCount: number;
deletedRecordInTable: string;
deletedOrgInvitationInfo?: object[]
} = {
deletedRecordsCount: records,
deletedRecordInTable: tableName
};

if (deletedOrgInvitationInfo) {
txnMetadata.deletedOrgInvitationInfo = deletedOrgInvitationInfo;
}

const recordType = RecordType.ORGANIZATION;
await this.userActivityRepository._orgDeletedActivity(orgId, user, txnMetadata, recordType);
}
Expand All @@ -1552,6 +1590,27 @@ export class OrganizationService {
throw new RpcException(error.response ?? error);
}
}


async sendEmailForOrgInvitationsMember(email: string, orgName: string, orgRole: string[]): Promise<boolean> {
const platformConfigData = await this.prisma.platform_config.findMany();
const urlEmailTemplate = new DeleteOrgInvitationsEmail();
const emailData = new EmailDto();
emailData.emailFrom = platformConfigData[0].emailFrom;
emailData.emailTo = email;
emailData.emailSubject = `Removal of participation of “${orgName}”`;

emailData.emailHtml = await urlEmailTemplate.sendDeleteOrgMemberEmailTemplate(
email,
orgName,
orgRole
);

//Email is sent to user for the verification through emailData
const isEmailSent = await sendEmail(emailData);

return isEmailSent;
}

async _deleteWallet(payload: IOrgAgent): Promise<{
response;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export class DeleteOrgInvitationsEmail {

public sendDeleteOrgMemberEmailTemplate(
email: string,
orgName: string,
orgRoles: string[]
): string {

const orgRoleNames = 0 < orgRoles.length ? orgRoles.join(', ') : '';
return `<!DOCTYPE html>
<html lang="en">

<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body style="margin: 0px; padding:0px; background-color:#F9F9F9;">
<div style="margin: auto; max-width: 450px; padding: 20px 30px; background-color: #FFFFFF; display:block;">
<div style="display: block; text-align:center; background-color: white; padding-bottom: 20px; padding-top: 20px;">
<img src="${process.env.BRAND_LOGO}" alt="${process.env.PLATFORM_NAME} logo" style="max-width:100px; background: white; padding: 5px;border-radius: 5px;" width="100%" height="fit-content" class="CToWUd" data-bit="iit">
</div>

<div style="font-family: Montserrat; font-style: normal; font-weight: 500;
font-size: 15px; line-height: 24px;color: #00000;">
<p style="margin-top:0px">
Hello ${email},
</p>
<p>
We would like to inform you that the organization “${orgName}” has removed their participation as a ${orgRoleNames} on CREDEBL.

<hr style="border-top:1px solid #e8e8e8" />
<footer style="padding-top: 10px;">
<div style="font-style: italic; color: #777777">
For any assistance or questions while accessing your account, please do not hesitate to contact the support team at ${process.env.PUBLIC_PLATFORM_SUPPORT_EMAIL}.
</div>
<p style="margin-top: 6px;">
© ${process.env.POWERED_BY}
</p>
</footer>
</div>
</div>
</body>

</html>`;

}


}
6 changes: 6 additions & 0 deletions apps/user/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,10 @@ export interface IUserDeletedActivity {
txnMetadata: Prisma.JsonValue;
deletedBy: string;
deleteDateTime: Date;
}

export interface UserKeycloakId {
id: string;
keycloakUserId: string;
email: string;
}
40 changes: 23 additions & 17 deletions apps/user/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
IUsersProfile,
IUserInformation,
IVerifyUserEmail,
IUserDeletedActivity
IUserDeletedActivity,
UserKeycloakId
} from '../interfaces/user.interface';
import { InternalServerErrorException } from '@nestjs/common';
import { PrismaService } from '@credebl/prisma-service';
Expand Down Expand Up @@ -798,30 +799,35 @@ export class UserRepository {
}
}

async getUserKeycloak(userEmails: string[]): Promise<string[]> {
async getUserKeycloak(userEmails: string[]): Promise<UserKeycloakId[]> {
try {
const users = await this.prisma.user.findMany({
where: {
email: {
in: userEmails
}
email: {
in: userEmails
}
},
select: {
email: true,
keycloakUserId: true
email: true,
keycloakUserId: true,
id: true
}
});

// Create a map for quick lookup of keycloakUserId by email
const userMap = new Map(users.map(user => [user.email, user.keycloakUserId]));

// Collect the keycloakUserIds in the order of input emails
const keycloakUserIds = userEmails.map(email => userMap.get(email) || null);

return keycloakUserIds;
});

// Create a map for quick lookup of keycloakUserId, id, and email by email
const userMap = new Map(users.map(user => [user.email, { id: user.id, keycloakUserId: user.keycloakUserId, email: user.email }]));

// Collect the keycloakUserId, id, and email in the order of input emails
const result = userEmails.map(email => {
const user = userMap.get(email);
return { id: user?.id || null, keycloakUserId: user?.keycloakUserId || null, email };
});

return result;
} catch (error) {
this.logger.error(`Error in getUserKeycloak: ${error} `);
this.logger.error(`Error in getUserKeycloak: ${error}`);
throw error;
}
}

}
Loading