diff --git a/apps/api-gateway/src/verification/dto/request-proof.dto.ts b/apps/api-gateway/src/verification/dto/request-proof.dto.ts index ac7f8d705..9e1f49174 100644 --- a/apps/api-gateway/src/verification/dto/request-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/request-proof.dto.ts @@ -1,8 +1,7 @@ +import { ArrayNotEmpty, IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsObject, IsOptional, IsString, ValidateIf, ValidateNested, IsUUID, ArrayUnique, ArrayMaxSize } from 'class-validator'; +import { toLowerCase, trim } from '@credebl/common/cast.helper'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayNotEmpty, IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsObject, IsOptional, IsString, IsUUID, ValidateIf, ValidateNested } from 'class-validator'; import { Transform, Type } from 'class-transformer'; -import { toLowerCase, trim } from '@credebl/common/cast.helper'; - import { AutoAccept } from '@credebl/enum/enum'; import { IProofFormats } from '../interfaces/verification.interface'; @@ -333,6 +332,16 @@ export class SendProofRequestPayload { @IsNotEmpty({message:'Please provide the flag for shorten url.'}) isShortenUrl?: boolean; + @ApiPropertyOptional() + @IsEmail({}, { each: true, message: 'Please provide a valid email' }) + @ArrayNotEmpty({ message: 'Email array must not be empty' }) + @ArrayUnique({ message: 'Duplicate emails are not allowed' }) + @ArrayMaxSize(Number(process.env.OOB_BATCH_SIZE), { message: `Limit reached (${process.env.OOB_BATCH_SIZE} proof request max).` }) + @IsArray() + @IsString({ each: true, message: 'Each emailId in the array should be a string' }) + @IsOptional() + emailId: string[]; + @ApiPropertyOptional({ default: true }) @IsOptional() @IsNotEmpty({ message: 'please provide valid value for reuseConnection' }) diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index f883e49c2..b4f286580 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -1,5 +1,5 @@ -import { AutoAccept } from "@credebl/enum/enum"; -import { IUserRequest } from "@credebl/user-request/user-request.interface"; +import { AutoAccept } from '@credebl/enum/enum'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; interface IProofRequestAttribute { attributeName?: string; @@ -144,6 +144,7 @@ export interface ISendProofRequestPayload { parentThreadId?: string; willConfirm?: boolean; imageUrl?: string; + emailId?: string[] isShortenUrl?: boolean; type?:string; presentationDefinition?:IProofRequestPresentationDefinition; @@ -162,6 +163,7 @@ export interface IWSendProofRequestPayload { parentThreadId?: string; willConfirm?: boolean; imageUrl?: string; + emailId?: string[]; type?:string; presentationDefinition?:IProofRequestPresentationDefinition; } @@ -169,7 +171,7 @@ export interface IWSendProofRequestPayload { export interface IProofRequestPayload { url: string; apiKey: string; - proofRequestPayload: ISendProofRequestPayload; + proofRequestPayload: ISendProofRequestPayload | ISendPresentationExchangeProofRequestPayload; } interface IWebhookPresentationProof { @@ -213,3 +215,6 @@ export interface IProofRequestSearchCriteria { searchByText: string; } +export interface IInvitation{ + invitationUrl?: string; +} \ No newline at end of file diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index 5b5b0e222..b76b191b7 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs/operators'; -import { IGetAllProofPresentations, IProofRequestSearchCriteria, IGetProofPresentationById, IProofPresentation, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IVerifiedProofData, IPresentationExchangeProofRequestPayload} from './interfaces/verification.interface'; +import { IGetAllProofPresentations, IProofRequestSearchCriteria, IGetProofPresentationById, IProofPresentation, IProofRequestPayload, IRequestProof, ISendProofRequestPayload, IVerifyPresentation, IVerifiedProofData, IInvitation} from './interfaces/verification.interface'; import { VerificationRepository } from './repositories/verification.repository'; import { CommonConstants } from '@credebl/common/common.constant'; import { agent_invitations, org_agents, organisation, presentations } from '@prisma/client'; @@ -350,6 +350,7 @@ export class VerificationService { } outOfBandRequestProof['label'] = label; + const orgAgentType = await this.verificationRepository.getOrgAgentType(getAgentDetails?.orgAgentTypeId); let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); const verificationMethodLabel = 'create-request-out-of-band'; @@ -358,7 +359,8 @@ export class VerificationService { apiKey = await this._getOrgAgentApiKey(user.orgId); } - const { isShortenUrl, type, reuseConnection, ...updateOutOfBandRequestProof } = outOfBandRequestProof; + // Destructuring 'outOfBandRequestProof' to remove emailId, as it is not used while agent operation + const { isShortenUrl, emailId, type, reuseConnection, ...updateOutOfBandRequestProof } = outOfBandRequestProof; let recipientKey: string | undefined; if (true === reuseConnection) { const data: agent_invitations[] = await this.verificationRepository.getRecipientKeyByOrgId(user.orgId); @@ -369,7 +371,8 @@ export class VerificationService { } outOfBandRequestProof.autoAcceptProof = outOfBandRequestProof.autoAcceptProof || 'always'; - let payload: IProofRequestPayload | IPresentationExchangeProofRequestPayload; + + let payload: IProofRequestPayload; if (ProofRequestType.INDY === type) { updateOutOfBandRequestProof.protocolVersion = updateOutOfBandRequestProof.protocolVersion || 'v1'; @@ -398,37 +401,30 @@ export class VerificationService { } } }, - autoAcceptProof:outOfBandRequestProof.autoAcceptProof || 'always', + autoAcceptProof:outOfBandRequestProof.autoAcceptProof, recipientKey:recipientKey || undefined } - }; + }; } - - const getProofPresentation = await this._sendOutOfBandProofRequest(payload); - //apply presentation shorting URL - if (isShortenUrl) { - const proofRequestInvitationUrl: string = getProofPresentation?.response?.invitationUrl; - const shortenedUrl: string = await this.storeVerificationObjectAndReturnUrl(proofRequestInvitationUrl, false); - this.logger.log('shortenedUrl', shortenedUrl); - if (shortenedUrl) { - getProofPresentation.response.invitationUrl = shortenedUrl; + if (emailId) { + await this.sendEmailInBatches(payload, emailId, getAgentDetails, getOrganization); + return true; + } else { + const presentationProof: IInvitation = await this.generateOOBProofReq(payload, getAgentDetails); + const proofRequestInvitationUrl: string = presentationProof.invitationUrl; + if (isShortenUrl) { + const shortenedUrl: string = await this.storeVerificationObjectAndReturnUrl(proofRequestInvitationUrl, false); + this.logger.log('shortenedUrl', shortenedUrl); + if (shortenedUrl) { + presentationProof.invitationUrl = shortenedUrl; + } } - } - if (!getProofPresentation) { - throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); - } - return getProofPresentation.response; - - // Unused code : to be segregated - // if (outOfBandRequestProof.emailId) { - // const batchSize = 100; // Define the batch size according to your needs - // const { emailId } = outOfBandRequestProof; // Assuming it's an array - // await this.sendEmailInBatches(payload, emailId, getAgentDetails, organizationDetails, batchSize); - // return true; - // } else { - // return this.generateOOBProofReq(payload, getAgentDetails); - // } + if (!presentationProof) { + throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); + } + return presentationProof; + } } catch (error) { this.logger.error(`[sendOutOfBandPresentationRequest] - error in out of band proof request : ${error.message}`); this.verificationErrorHandling(error); @@ -460,34 +456,34 @@ export class VerificationService { } - async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[] | string, getAgentDetails: org_agents, organizationDetails: organisation, batchSize: number): Promise { + // Currently batch size is not used, as length of emails sent is restricted to '10' + async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[], getAgentDetails: org_agents, organizationDetails: organisation): Promise { + try { const accumulatedErrors = []; - if (Array.isArray(emailIds)) { - - for (let i = 0; i < emailIds.length; i += batchSize) { - const batch = emailIds.slice(i, i + batchSize); - const emailPromises = batch.map(async email => { + for (const email of emailIds) { try { - await this.sendOutOfBandProofRequest(payload, email, getAgentDetails, organizationDetails); - } catch (error) { - accumulatedErrors.push(error); - } - }); - - await Promise.all(emailPromises); - } - } else { - await this.sendOutOfBandProofRequest(payload, emailIds, getAgentDetails, organizationDetails); - } + await this.sendOutOfBandProofRequest(payload, email, getAgentDetails, organizationDetails); + await this.delay(500); + } catch (error) { + this.logger.error(`Error sending email to ${email}::::::`, error); + accumulatedErrors.push(error); + } + } if (0 < accumulatedErrors.length) { this.logger.error(accumulatedErrors); throw new Error(ResponseMessages.verification.error.emailSend); } + + } catch (error) { + this.logger.error('[sendEmailInBatches] - error in sending email in batches'); + throw new Error(ResponseMessages.verification.error.batchEmailSend); + } } + // This function is specifically for OOB verification using email async sendOutOfBandProofRequest(payload: IProofRequestPayload, email: string, getAgentDetails: org_agents, organizationDetails: organisation): Promise { let agentApiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); if (!agentApiKey || null === agentApiKey || undefined === agentApiKey) { @@ -500,16 +496,10 @@ export class VerificationService { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - const invitationId = getProofPresentation?.response?.invitation['@id']; - - if (!invitationId) { - throw new Error(ResponseMessages.verification.error.invitationNotFound); - } - - const shortenedUrl = getAgentDetails?.tenantId - ? `${getAgentDetails?.agentEndPoint}/multi-tenancy/url/${getAgentDetails?.tenantId}/${invitationId}` - : `${getAgentDetails?.agentEndPoint}/url/${invitationId}`; - + const invitationUrl = getProofPresentation?.response?.invitationUrl; + // Currently have shortenedUrl to store only for 30 days + const persist: boolean = false; + const shortenedUrl = await this.storeVerificationObjectAndReturnUrl(invitationUrl, persist); const qrCodeOptions: QRCode.QRCodeToDataURLOptions = { type: 'image/png' }; const outOfBandVerificationQrCode = await QRCode.toDataURL(shortenedUrl, qrCodeOptions); @@ -546,11 +536,11 @@ export class VerificationService { * @param payload * @returns Get requested proof presentation details */ - async _sendOutOfBandProofRequest(payload: IProofRequestPayload | IPresentationExchangeProofRequestPayload): Promise<{ + async _sendOutOfBandProofRequest(payload: IProofRequestPayload): Promise<{ response; }> { try { - + const pattern = { cmd: 'agent-send-out-of-band-proof-request' }; @@ -926,23 +916,26 @@ export class VerificationService { async natsCall(pattern: object, payload: object): Promise<{ response: string; }> { - return this.verificationServiceProxy - .send(pattern, payload) - .pipe( - map((response) => ( - { - response - })) - ) - .toPromise() - .catch(error => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.statusCode, - error: error.error, - message: error.message - }, error.error); - }); + return this.verificationServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ( + { + response + })) + ) + .toPromise() + .catch(error => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException({ + status: error.statusCode, + error: error.error, + message: error.message + }, error.error); + }); + } + + async delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); } - } \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 1aac2c206..5363cdd80 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -302,6 +302,7 @@ export const ResponseMessages = { proofNotFound: 'Proof presentation not found', invitationNotFound: 'Invitation not found', platformConfigNotFound: 'Platform config not found', + batchEmailSend: 'Unable to send email in batches', emailSend: 'Unable to send email to the user' } },