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 oob verification #577

Merged
merged 12 commits into from
Mar 20, 2024
15 changes: 12 additions & 3 deletions apps/api-gateway/src/verification/dto/request-proof.dto.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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' })
Expand Down
11 changes: 8 additions & 3 deletions apps/verification/src/interfaces/verification.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -144,6 +144,7 @@ export interface ISendProofRequestPayload {
parentThreadId?: string;
willConfirm?: boolean;
imageUrl?: string;
emailId?: string[]
isShortenUrl?: boolean;
type?:string;
presentationDefinition?:IProofRequestPresentationDefinition;
Expand All @@ -162,14 +163,15 @@ export interface IWSendProofRequestPayload {
parentThreadId?: string;
willConfirm?: boolean;
imageUrl?: string;
emailId?: string[];
type?:string;
presentationDefinition?:IProofRequestPresentationDefinition;
}

export interface IProofRequestPayload {
url: string;
apiKey: string;
proofRequestPayload: ISendProofRequestPayload;
proofRequestPayload: ISendProofRequestPayload | ISendPresentationExchangeProofRequestPayload;
}

interface IWebhookPresentationProof {
Expand Down Expand Up @@ -213,3 +215,6 @@ export interface IProofRequestSearchCriteria {
searchByText: string;
}

export interface IInvitation{
invitationUrl?: string;
}
145 changes: 69 additions & 76 deletions apps/verification/src/verification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -460,34 +456,34 @@ export class VerificationService {
}


async sendEmailInBatches(payload: IProofRequestPayload, emailIds: string[] | string, getAgentDetails: org_agents, organizationDetails: organisation, batchSize: number): Promise<void> {
// 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<void> {
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<boolean> {
let agentApiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY);
if (!agentApiKey || null === agentApiKey || undefined === agentApiKey) {
Expand All @@ -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);

Expand Down Expand Up @@ -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'
};
Expand Down Expand Up @@ -926,23 +916,26 @@ export class VerificationService {
async natsCall(pattern: object, payload: object): Promise<{
response: string;
}> {
return this.verificationServiceProxy
.send<string>(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<string>(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<unknown> {
return new Promise(resolve => setTimeout(resolve, ms));
}

}
1 change: 1 addition & 0 deletions libs/common/src/response-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
},
Expand Down