From e5f7289e408b02684cf525a87e69cfad306ae078 Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Mon, 4 Mar 2024 13:15:45 +0530 Subject: [PATCH 1/3] fix: solved the bulk issuance flow and function separation in bulk issuance Signed-off-by: KulkarniShashank --- .../interfaces/issuance.interfaces.ts | 17 + apps/issuance/src/issuance.service.ts | 486 ++++++++++-------- 2 files changed, 284 insertions(+), 219 deletions(-) diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index fdb743271..5cb4caa75 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -240,3 +240,20 @@ export interface OrgAgent { orgAgentTypeId: string; tenantId: string; } + +export interface SendEmailCredentialOffer { + iterator: CredentialOffer; + emailId: string; + index: number; + credentialType: IssueCredentialType; + protocolVersion: string; + attributes: IAttributes[]; + credentialDefinitionId: string; + outOfBandCredential: OutOfBandCredentialOfferPayload; + comment: string; + organisation: organisation; + errors; + url: string; + apiKey: string; + organizationDetails: organisation; +} \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 0b4ff8ddd..52d8bf709 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -9,7 +9,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; // import { ClientDetails, FileUploadData, ICredentialAttributesInterface, ImportFileDetails, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; -import { FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, IIssuance, IIssueData, IPattern, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails } from '../interfaces/issuance.interfaces'; +import { CredentialOffer, FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, IIssuance, IIssueData, IPattern, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer } from '../interfaces/issuance.interfaces'; import { OrgAgentType } from '@credebl/enum/enum'; // import { platform_config } from '@prisma/client'; import * as QRCode from 'qrcode'; @@ -32,6 +32,7 @@ import { io } from 'socket.io-client'; import { IIssuedCredentialSearchParams, IssueCredentialType } from 'apps/api-gateway/src/issuance/interfaces'; import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; +import { organisation } from '@prisma/client'; @Injectable() @@ -425,259 +426,305 @@ export class IssuanceService { } } - - async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload): Promise { - try { - const { - credentialOffer, - comment, - credentialDefinitionId, - orgId, - protocolVersion, - attributes, - emailId, - credentialType - } = outOfBandCredential; - +async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload): Promise { + try { + const { + credentialOffer, + comment, + credentialDefinitionId, + orgId, + protocolVersion, + attributes, + emailId, + credentialType + } = outOfBandCredential; if (IssueCredentialType.INDY === credentialType) { + const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); - const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( - credentialDefinitionId - ); - - let attributesArray:IAttributes[] = []; - if (schemaResponse?.attributes) { - - attributesArray = JSON.parse(schemaResponse.attributes); - } - - if (0 < attributes?.length) { -const attrError = []; - attributesArray.forEach((schemaAttribute, i) => { - if (schemaAttribute.isRequired) { + let attributesArray: IAttributes[] = []; + if (schemaResponse?.attributes) { + attributesArray = JSON.parse(schemaResponse.attributes); + } - const attribute = attributes.find(attribute => attribute.name === schemaAttribute.attributeName); - if (!attribute?.value) { - attrError.push( - `attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` - ); - } - - } - - }); - if (0 < attrError.length) { - throw new BadRequestException(attrError); + if (0 < attributes?.length) { + const attrError = []; + attributesArray.forEach((schemaAttribute, i) => { + if (schemaAttribute.isRequired) { + const attribute = attributes.find((attribute) => attribute.name === schemaAttribute.attributeName); + if (!attribute?.value) { + attrError.push(`attributes.${i}.Attribute ${schemaAttribute.attributeName} is required`); } + } + }); + if (0 < attrError.length) { + throw new BadRequestException(attrError); } - if (0 < credentialOffer?.length) { -const credefError = []; - credentialOffer.forEach((credentialAttribute, index) => { - - attributesArray.forEach((schemaAttribute, i) => { - - const attribute = credentialAttribute.attributes.find(attribute => attribute.name === schemaAttribute.attributeName); - - if (schemaAttribute.isRequired && !attribute?.value) { - credefError.push( - `credentialOffer.${index}.attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` - ); - } - - }); - }); - if (0 < credefError.length) { - throw new BadRequestException(credefError); + } + if (0 < credentialOffer?.length) { + const credefError = []; + credentialOffer.forEach((credentialAttribute, index) => { + attributesArray.forEach((schemaAttribute, i) => { + const attribute = credentialAttribute.attributes.find( + (attribute) => attribute.name === schemaAttribute.attributeName + ); + + if (schemaAttribute.isRequired && !attribute?.value) { + credefError.push( + `credentialOffer.${index}.attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` + ); } + }); + }); + if (0 < credefError.length) { + throw new BadRequestException(credefError); } - } - - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - - const { organisation } = agentDetails; - if (!agentDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); - } - - const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - - const issuanceMethodLabel = 'create-offer-oob'; - const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentDetails.agentEndPoint, agentDetails.tenantId); - const organizationDetails = await this.issuanceRepository.getOrganization(orgId); - - if (!organizationDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); } + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); - if (!apiKey || null === apiKey || undefined === apiKey) { - apiKey = await this._getOrgAgentApiKey(orgId); - } + const { organisation } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } - const errors = []; - const emailPromises = []; - let outOfBandIssuancePayload; - const sendEmailForCredentialOffer = async (iterator, emailId, index): Promise => { - const iterationNo = index + 1; - try { - if (IssueCredentialType.INDY === credentialType) { - - outOfBandIssuancePayload = { - protocolVersion: protocolVersion || 'v1', - credentialFormats: { - indy: { - attributes: iterator.attributes || attributes, - credentialDefinitionId - } - }, - autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', - comment, - goalCode: outOfBandCredential.goalCode || undefined, - parentThreadId: outOfBandCredential.parentThreadId || undefined, - willConfirm: outOfBandCredential.willConfirm || undefined, - label: outOfBandCredential.label || undefined, - imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl - }; - } + const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - if (IssueCredentialType.JSONLD === credentialType) { - outOfBandIssuancePayload = { - protocolVersion:'v2', - credentialFormats: { - jsonld: { - credential: iterator.credential, - options: iterator.options - } - }, - autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', - comment, - goalCode: outOfBandCredential.goalCode || undefined, - parentThreadId: outOfBandCredential.parentThreadId || undefined, - willConfirm: outOfBandCredential.willConfirm || undefined, - label: outOfBandCredential.label || undefined, - imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl - }; - } - + const issuanceMethodLabel = 'create-offer-oob'; + const url = await this.getAgentUrl( + issuanceMethodLabel, + orgAgentType, + agentDetails.agentEndPoint, + agentDetails.tenantId + ); + const organizationDetails = await this.issuanceRepository.getOrganization(orgId); - this.logger.log(`outOfBandIssuancePayload ::: ${JSON.stringify(outOfBandIssuancePayload)}`); + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); + } - const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, apiKey); + let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); + if (!apiKey || null === apiKey || undefined === apiKey) { + apiKey = await this._getOrgAgentApiKey(orgId); + } - if (!credentialCreateOfferDetails) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); - return false; - } + const errors = []; + const emailPromises = []; + const sendEmailCredentialOffer: { + iterator: CredentialOffer; + emailId: string; + index: number; + credentialType: IssueCredentialType; + protocolVersion: string; + attributes: IAttributes[]; + credentialDefinitionId: string; + outOfBandCredential: OutOfBandCredentialOfferPayload; + comment: string; + organisation: organisation; + errors: string[]; + url: string; + apiKey: string; + organizationDetails: organisation; + } = { + credentialType, + protocolVersion, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + apiKey, + organizationDetails, + iterator: undefined, + emailId: emailId || '', + index: 0 + }; - const invitationId = credentialCreateOfferDetails.response.invitation['@id']; + if (credentialOffer) { + for (let i = 0; i < credentialOffer.length; i += Number(process.env.OOB_BATCH_SIZE)) { + const batch = credentialOffer.slice(i, i + Number(process.env.OOB_BATCH_SIZE)); + // Process each batch in parallel + const batchPromises = batch.map(async (iterator, index) => { + + sendEmailCredentialOffer['iterator'] = iterator; + sendEmailCredentialOffer['emailId'] = iterator.emailId; + sendEmailCredentialOffer['index'] = index; - if (!invitationId) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); - return false; - } + return this.sendEmailForCredentialOffer(sendEmailCredentialOffer); + }); + emailPromises.push(Promise.all(batchPromises)); + } + } else { + emailPromises.push(this.sendEmailForCredentialOffer(sendEmailCredentialOffer)); + } - const agentEndPoint = agentDetails.tenantId - ? `${agentDetails.agentEndPoint}/multi-tenancy/url/${agentDetails.tenantId}/${invitationId}` - : `${agentDetails.agentEndPoint}/url/${invitationId}`; + const results = await Promise.all(emailPromises); - const qrCodeOptions = { type: 'image/png' }; - const outOfBandIssuanceQrCode = await QRCode.toDataURL(agentEndPoint, qrCodeOptions); - const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + // Flatten the results array + const flattenedResults = [].concat(...results); - if (!platformConfigData) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); - return false; - } + // Check if all emails were successfully sent + const allSuccessful = flattenedResults.every((result) => true === result); - this.emailData.emailFrom = platformConfigData.emailFrom; - this.emailData.emailTo = emailId; - this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Issuance of Your Credential`; - this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, agentEndPoint); - this.emailData.emailAttachments = [ - { - filename: 'qrcode.png', - content: outOfBandIssuanceQrCode.split(';base64,')[1], - contentType: 'image/png', - disposition: 'attachment' - } - ]; + if (0 < errors.length) { + throw errors; + } - const isEmailSent = await sendEmail(this.emailData); - this.logger.log(`isEmailSent ::: ${JSON.stringify(isEmailSent)}`); + return allSuccessful; + } catch (error) { + this.logger.error( + `[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}` + ); + if (0 < error?.length) { + const errorStack = error?.map((item) => { + const { message, statusCode, error } = item?.error || item?.response || {}; + return { + message, + statusCode, + error + }; + }); + throw new RpcException({ + error: errorStack, + statusCode: error?.status?.code, + message: ResponseMessages.issuance.error.unableToCreateOOBOffer + }); + } else { + throw new RpcException(error.response ? error.response : error); + } + } +} - if (!isEmailSent) { - errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); - return false; +async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialOffer): Promise { + const { + iterator, + emailId, + index, + credentialType, + protocolVersion, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + apiKey, + organizationDetails + } = sendEmailCredentialOffer; + + const iterationNo = index + 1; + try { + let outOfBandIssuancePayload; + if (IssueCredentialType.INDY === credentialType) { + + outOfBandIssuancePayload = { + protocolVersion: protocolVersion || 'v1', + credentialFormats: { + indy: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: attributes ? attributes : iterator.attributes.map(({ isRequired, ...rest }) => rest), + credentialDefinitionId } + }, + autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: outOfBandCredential.label || undefined, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl + }; + } - return isEmailSent; - } catch (error) { - this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); - const errorStack = error?.status?.message; - if (errorStack) { - errors.push( - new RpcException({ - error: `${errorStack?.error?.message} at position ${iterationNo}`, - statusCode: errorStack?.statusCode, - message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}` - })); - } else { - errors.push(new InternalServerErrorException(`${error.message} at position ${iterationNo}`)); + if (IssueCredentialType.JSONLD === credentialType) { + outOfBandIssuancePayload = { + protocolVersion: 'v2', + credentialFormats: { + jsonld: { + credential: iterator.credential, + options: iterator.options } - return false; - } + }, + autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: outOfBandCredential.label || undefined, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl }; + } - if (credentialOffer) { - for (let i = 0; i < credentialOffer.length; i += Number(process.env.OOB_BATCH_SIZE)) { - const batch = credentialOffer.slice(i, i + Number(process.env.OOB_BATCH_SIZE)); - - // Process each batch in parallel - const batchPromises = batch.map((iterator, index) => sendEmailForCredentialOffer(iterator, iterator.emailId, index)); - emailPromises.push(Promise.all(batchPromises)); - } - } else { - emailPromises.push(sendEmailForCredentialOffer({}, emailId, 1)); - } - - const results = await Promise.all(emailPromises); + const agentDetails = await this.issuanceRepository.getAgentEndPoint(organisation.id); - // Flatten the results array - const flattenedResults = [].concat(...results); + this.logger.log(`outOfBandIssuancePayload ::: ${JSON.stringify(outOfBandIssuancePayload)}`); - // Check if all emails were successfully sent - const allSuccessful = flattenedResults.every((result) => true === result); + const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, apiKey); - if (0 < errors.length) { - throw errors; - } + if (!credentialCreateOfferDetails) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); + return false; + } - return allSuccessful; - } catch (error) { - this.logger.error(`[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}`); - if (0 < error?.length) { - const errorStack = error?.map(item => { - const { message, statusCode, error } = item?.error || item?.response || {}; - return { - message, - statusCode, - error - }; - }); - throw new RpcException({ - error: errorStack, - statusCode: error?.status?.code, - message: ResponseMessages.issuance.error.unableToCreateOOBOffer - }); - } else { - throw new RpcException(error.response ? error.response : error); - } + const invitationId = credentialCreateOfferDetails.response.invitation['@id']; + if (!invitationId) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); + return false; + } + const agentEndPoint = agentDetails.tenantId + ? `${agentDetails.agentEndPoint}/multi-tenancy/url/${agentDetails.tenantId}/${invitationId}` + : `${agentDetails.agentEndPoint}/url/${invitationId}`; + const qrCodeOptions = { type: 'image/png' }; + const outOfBandIssuanceQrCode = await QRCode.toDataURL(agentEndPoint, qrCodeOptions); + const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + if (!platformConfigData) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); + return false; + } + this.emailData.emailFrom = platformConfigData.emailFrom; + this.emailData.emailTo = emailId; + this.emailData.emailSubject = `${process.env.PLATFORM_NAME} Platform: Issuance of Your Credential`; + this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, agentEndPoint); + this.emailData.emailAttachments = [ + { + filename: 'qrcode.png', + content: outOfBandIssuanceQrCode.split(';base64,')[1], + contentType: 'image/png', + disposition: 'attachment' + } + ]; + const isEmailSent = await sendEmail(this.emailData); + this.logger.log(`isEmailSent ::: ${JSON.stringify(isEmailSent)}`); + if (!isEmailSent) { + errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); + return false; + } + return isEmailSent; + + } catch (error) { + this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); + const errorStack = error?.status?.message; + if (errorStack) { + errors.push( + new RpcException({ + error: `${errorStack?.error?.message} at position ${iterationNo}`, + statusCode: errorStack?.statusCode, + message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}` + }) + ); + } else { + errors.push(new InternalServerErrorException(`${error.message} at position ${iterationNo}`)); } + return false; } +} - async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, apiKey: string): Promise<{ response; }> { @@ -1161,7 +1208,8 @@ const credefError = []; orgId: jobDetails.orgId, label: organisation?.name, attributes: [], - emailId: jobDetails.data.email + emailId: jobDetails.data.email, + credentialType: IssueCredentialType.INDY }; for (const key in jobDetails.data) { From 3b04d614023a9ddf484bc129eeb3c9178a738dfe Mon Sep 17 00:00:00 2001 From: KulkarniShashank Date: Mon, 4 Mar 2024 16:36:07 +0530 Subject: [PATCH 2/3] Added the bin bash in shell script Signed-off-by: KulkarniShashank --- apps/agent-provisioning/AFJ/scripts/start_agent.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/agent-provisioning/AFJ/scripts/start_agent.sh b/apps/agent-provisioning/AFJ/scripts/start_agent.sh index fbb449988..87a595939 100755 --- a/apps/agent-provisioning/AFJ/scripts/start_agent.sh +++ b/apps/agent-provisioning/AFJ/scripts/start_agent.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash START_TIME=$(date +%s) From 24897e290e211ef151f8795c51594f93d68b50c9 Mon Sep 17 00:00:00 2001 From: "rohit.shitre" Date: Mon, 4 Mar 2024 22:56:08 +0530 Subject: [PATCH 3/3] fix:removed multiple promise all from issuance flow. Signed-off-by: rohit.shitre --- apps/issuance/src/issuance.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 52d8bf709..f54a8031d 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -557,7 +557,7 @@ async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayl return this.sendEmailForCredentialOffer(sendEmailCredentialOffer); }); - emailPromises.push(Promise.all(batchPromises)); + emailPromises.push(batchPromises); } } else { emailPromises.push(this.sendEmailForCredentialOffer(sendEmailCredentialOffer));