From 7421fba6494f2d339b160760973d2752a19c1a5e Mon Sep 17 00:00:00 2001 From: pranalidhanavade <137780597+pranalidhanavade@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:36:26 +0530 Subject: [PATCH] fix: issuance API with required attribute (#528) * fix:issuance api Signed-off-by: pranalidhanavade * fix:issuance api issue Signed-off-by: pranalidhanavade * fix:sonarlint issues Signed-off-by: pranalidhanavade * fix:requried attribute in issuance api Signed-off-by: pranalidhanavade * fix:requried attributes in csv file Signed-off-by: pranalidhanavade --------- Signed-off-by: pranalidhanavade --- .../src/issuance/dtos/issuance.dto.ts | 22 +- apps/issuance/src/issuance.service.ts | 261 +++++++++++------- 2 files changed, 168 insertions(+), 115 deletions(-) diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index 0d81ebe75..5b7957137 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -20,10 +20,11 @@ class Attribute { @Transform(({ value }) => trim(value)) value: string; - @ApiProperty() + @ApiProperty({ default: false }) @IsBoolean() + @IsOptional() @IsNotEmpty({ message: 'isRequired property is required' }) - isRequired: boolean; + isRequired?: boolean = false; } @@ -87,8 +88,7 @@ export class OOBIssueCredentialDto extends CredentialsIssuanceDto { example: [ { value: 'string', - name: 'string', - isRequired: 'boolean' + name: 'string' } ] }) @@ -100,14 +100,14 @@ export class OOBIssueCredentialDto extends CredentialsIssuanceDto { } class CredentialOffer { - @ApiProperty({ example: [{ 'value': 'string', 'name': 'string', 'isRequired':'boolean' }] }) + @ApiProperty({ example: [{ 'value': 'string', 'name': 'string' }] }) @IsNotEmpty({ message: 'Attribute name is required' }) @IsArray({ message: 'Attributes should be an array' }) @ValidateNested({ each: true }) @Type(() => Attribute) attributes: Attribute[]; - @ApiProperty({ example: 'testmail@mailinator.com' }) + @ApiProperty({ example: 'testmail@xyz.com' }) @IsEmail({}, { message: 'Please provide a valid email' }) @IsNotEmpty({ message: 'Email is required' }) @IsString({ message: 'Email should be a string' }) @@ -212,7 +212,7 @@ export class CredentialAttributes { } export class OOBCredentialDtoWithEmail { - @ApiProperty({ example: [{ 'emailId': 'abc@example.com', 'attributes': [{ 'value': 'string', 'name': 'string', 'isRequired':'boolean' }] }] }) + @ApiProperty({ example: [{ 'emailId': 'abc@example.com', 'attributes': [{ 'value': 'string', 'name': 'string' }] }] }) @IsNotEmpty({ message: 'Please provide valid attributes' }) @IsArray({ message: 'attributes should be array' }) @ArrayMaxSize(Number(process.env.OOB_BATCH_SIZE), { message: `Limit reached (${process.env.OOB_BATCH_SIZE} credentials max). Easily handle larger batches via seamless CSV file uploads` }) @@ -220,14 +220,6 @@ export class OOBCredentialDtoWithEmail { @Type(() => CredentialOffer) credentialOffer: CredentialOffer[]; - @ApiProperty({ example: 'awqx@getnada.com' }) - @IsEmail({}, { message: 'Please provide a valid email' }) - @IsNotEmpty({ message: 'Please provide valid email' }) - @IsString({ message: 'email should be string' }) - @Transform(({ value }) => value.trim().toLowerCase()) - @IsOptional() - emailId: string; - @ApiProperty({ example: 'string' }) @IsNotEmpty({ message: 'Please provide valid credential definition id' }) @IsString({ message: 'credential definition id should be string' }) diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 68ca23842..f44d2c216 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, IClientDetails, ICreateOfferResponse, IIssuance, IIssueData, IPattern, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, 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 { OrgAgentType } from '@credebl/enum/enum'; // import { platform_config } from '@prisma/client'; import * as QRCode from 'qrcode'; @@ -51,26 +51,37 @@ export class IssuanceService { async sendCredentialCreateOffer(payload: IIssuance): Promise { + try { const { orgId, credentialDefinitionId, comment, connectionId, attributes } = payload || {}; - const attrError = []; - if (0 < attributes?.length) { - attributes?.forEach((attribute, i) => { - - if (attribute.isRequired && !attribute.value) { - attrError.push(`attributes.${i}.Value of "${attribute.name}" is required`); - return true; - } + const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); + + if (schemaResponse?.attributes) { + const schemaResponseError = []; + const attributesArray: IAttributes[] = JSON.parse(schemaResponse.attributes); + + attributesArray.forEach((attribute) => { + if (attribute.attributeName && attribute.isRequired) { - return attribute.isRequired && !attribute.value; - }); - - if (0 < attrError.length) { - throw new BadRequestException(attrError); - } + payload.attributes.map((attr) => { + if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { + schemaResponseError.push( + `Attribute ${attribute.attributeName} is required` + ); + } + return true; + }); + } + }); + if (0 < schemaResponseError.length) { + throw new BadRequestException(schemaResponseError); + } - + + } const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); @@ -133,28 +144,37 @@ export class IssuanceService { } } - - async sendCredentialOutOfBand(payload: OOBIssueCredentialDto): Promise<{ response: object; }> { + async sendCredentialOutOfBand(payload: OOBIssueCredentialDto): Promise<{ response: object }> { try { const { orgId, credentialDefinitionId, comment, attributes, protocolVersion } = payload; - const attrError = []; - if (0 < attributes?.length) { - attributes?.forEach((attribute, i) => { - - if (attribute.isRequired && !attribute.value) { - attrError.push(`attributes.${i}.Value of "${attribute.name}" is required`); - return true; - } + const schemadetailsResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); + + if (schemadetailsResponse?.attributes) { + const schemadetailsResponseError = []; + const attributesArray: IAttributes[] = JSON.parse(schemadetailsResponse.attributes); + + attributesArray.forEach((attribute) => { + if (attribute.attributeName && attribute.isRequired) { - return attribute.isRequired && !attribute.value; - }); - - if (0 < attrError.length) { - throw new BadRequestException(attrError); - } + payload.attributes.map((attr) => { + if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { + schemadetailsResponseError.push( + `Attribute '${attribute.attributeName}' is required but has an empty value.` + ); + } + return true; + }); + } + }); + if (0 < schemadetailsResponseError.length) { + throw new BadRequestException(schemadetailsResponseError); } + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); // eslint-disable-next-line camelcase @@ -239,8 +259,8 @@ export class IssuanceService { .pipe( map((response) => ( { - response - })) + response + })) ).toPromise() .catch(error => { this.logger.error(`catch: ${JSON.stringify(error)}`); @@ -387,26 +407,58 @@ export class IssuanceService { emailId } = outOfBandCredential; - const attrError = []; -if (0 < credentialOffer?.length) { - credentialOffer?.forEach((credential, i) => { - credential.attributes.forEach((attribute, i2) => { + const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( + credentialDefinitionId + ); - if (attribute.isRequired && !attribute.value) { - attrError.push(`credentialOffer.${i}.attributes.${i2}.Value of "${attribute.name}" is required`); - return true; - } - - return attribute.isRequired && !attribute.value; - }); + let attributesArray:IAttributes[] = []; + if (schemaResponse?.attributes) { - }); + attributesArray = JSON.parse(schemaResponse.attributes); + } - 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); + } + } + + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -422,9 +474,6 @@ if (0 < credentialOffer?.length) { throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); } - // if (!(credentialOffer && 0 < credentialOffer.length)) { - // throw new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound); - // } let apiKey: string = await this.cacheService.get(CommonConstants.CACHE_APIKEY_KEY); if (!apiKey || null === apiKey || undefined === apiKey) { @@ -494,10 +543,10 @@ if (0 < credentialOffer?.length) { 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; @@ -568,7 +617,7 @@ if (0 < credentialOffer?.length) { } } - + async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, apiKey: string): Promise<{ response; }> { @@ -583,10 +632,10 @@ if (0 < credentialOffer?.length) { } /** - * Description: Fetch agent url - * @param referenceId - * @returns agent URL - */ + * Description: Fetch agent url + * @param referenceId + * @returns agent URL + */ async getAgentUrl( issuanceMethodLabel: string, orgAgentType: string, @@ -600,8 +649,8 @@ if (0 < credentialOffer?.length) { switch (issuanceMethodLabel) { case 'create-offer': { url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` - : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` + : orgAgentType === OrgAgentType.SHARED ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER}`.replace('#', tenantId) : null; break; @@ -609,8 +658,8 @@ if (0 < credentialOffer?.length) { case 'create-offer-oob': { url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_OUT_OF_BAND_CREDENTIAL_OFFER}` - : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_OUT_OF_BAND_CREDENTIAL_OFFER}` + : orgAgentType === OrgAgentType.SHARED ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND}`.replace('#', tenantId) : null; break; @@ -618,18 +667,18 @@ if (0 < credentialOffer?.length) { case 'get-issue-credentials': { url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ}` - : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ}` + : orgAgentType === OrgAgentType.SHARED ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS}`.replace('#', tenantId) : null; break; } case 'get-issue-credential-by-credential-id': { - + url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID}/${credentialRecordId}` - : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID}/${credentialRecordId}` + : orgAgentType === OrgAgentType.SHARED ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS_BY_CREDENTIAL_ID}`.replace('#', credentialRecordId).replace('@', tenantId) : null; break; @@ -697,7 +746,7 @@ if (0 < credentialOffer?.length) { async importAndPreviewDataForIssuance(importFileDetails: ImportFileDetails, requestId?: string): Promise { try { - + const credDefResponse = await this.issuanceRepository.getCredentialDefinitionDetails(importFileDetails.credDefId); @@ -734,7 +783,7 @@ if (0 < credentialOffer?.length) { throw new BadRequestException(`Invalid emails found in the chosen file`); } - const fileData: string[] = parsedData.data.map(Object.values); + const fileData: string[][] = parsedData.data.map(Object.values); const fileHeader: string[] = parsedData.meta.fields; const attributesArray = JSON.parse(credDefResponse.attributes); @@ -749,7 +798,7 @@ if (0 < credentialOffer?.length) { } await this.validateFileHeaders(fileHeader, attributeNameArray); - await this.validateFileData(fileData); + await this.validateFileData(fileData, attributesArray, fileHeader); const resData = { schemaLedgerId: credDefResponse.schemaLedgerId, @@ -765,8 +814,8 @@ if (0 < credentialOffer?.length) { return newCacheKey; } catch (error) { - this.logger.error(`error in validating credentials : ${error}`); - throw new RpcException(error.response ? error.response : error); + this.logger.error(`error in validating credentials : ${error.response}`); + throw new RpcException(error.response ? error.response : error); } finally { // await this.awsService.deleteFile(importFileDetails.fileKey); // this.logger.error(`Deleted uploaded file after processing.`); @@ -892,11 +941,11 @@ if (0 < credentialOffer?.length) { } if (cachedData && clientDetails?.isSelectiveIssuance) { - await this.cacheManager.del(requestId); - await this.importAndPreviewDataForIssuance(reqPayload, requestId); + await this.cacheManager.del(requestId); + await this.importAndPreviewDataForIssuance(reqPayload, requestId); // await this.cacheManager.set(requestId, reqPayload); cachedData = await this.cacheManager.get(requestId); - } + } const parsedData = JSON.parse(cachedData as string).fileData.data; const parsedPrimeDetails = JSON.parse(cachedData as string); @@ -1117,7 +1166,6 @@ if (0 < credentialOffer?.length) { ): Promise { try { const fileSchemaHeader: string[] = fileHeader.slice(); - if ('email' === fileHeader[0]) { fileSchemaHeader.splice(0, 1); } else { @@ -1141,25 +1189,38 @@ if (0 < credentialOffer?.length) { } } - async validateFileData(fileData: string[]): Promise { - let rowIndex: number = 0; - let columnIndex: number = 0; - const isNullish = Object.values(fileData).some((value) => { - columnIndex = 0; - rowIndex++; - const isFalsyForColumnValue = Object.values(value).some((colvalue) => { - columnIndex++; - if (null === colvalue || '' == colvalue) { - return true; - } - return false; + async validateFileData(fileData: string[][], attributesArray: { attributeName: string, schemaDataType: string, displayName: string, isRequired: boolean }[], fileHeader: string[]): Promise { + try { + const filedata = fileData.map((item: string[]) => { + const fileHeaderData = item?.map((element, j) => ({ + value: element, + header: fileHeader[j] + })); + return fileHeaderData; }); - return isFalsyForColumnValue; - }); - if (isNullish) { - throw new BadRequestException( - `Empty data found at row ${rowIndex} and column ${columnIndex}` - ); + + const errorFileData = []; + + filedata.forEach((attr, i) => { + attr.forEach((eachElement) => { + + attributesArray.forEach((eachItem) => { + if (eachItem.attributeName === eachElement.header) { + if (eachItem.isRequired && !eachElement.value) { + errorFileData.push(`Attribute ${eachItem.attributeName} is required at row ${i + 1}`); + } + } + }); + return eachElement; + }); + return attr; + }); + + if (0 < errorFileData.length) { + throw new BadRequestException(errorFileData); + } + } catch (error) { + throw error; } } @@ -1174,9 +1235,9 @@ if (0 < credentialOffer?.length) { } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + status: error.status, + error: error.message + }, error.status); } }