Skip to content

Commit

Permalink
refactor: applied validations on schema url attributes for jsonld (#792)
Browse files Browse the repository at this point in the history
Signed-off-by: bhavanakarwade <bhavana.karwade@ayanworks.com>
Signed-off-by: KulkarniShashank <shashank.kulkarni@ayanworks.com>
  • Loading branch information
bhavanakarwade authored and KulkarniShashank committed Sep 11, 2024
1 parent 7b60f22 commit 4f53f4d
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 10 deletions.
12 changes: 11 additions & 1 deletion apps/issuance/interfaces/issuance.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,17 @@ export interface IQueuePayload{
isRetry: boolean;
isLastData: boolean;
}
export interface ISchemaAttributes {
attributeName: string;
schemaDataType: string;
displayName: string;
isRequired: boolean;
}

export interface IIssuanceAttributes {
[key: string]: string;
}
export interface IDeletedFileUploadRecords {
deleteFileDetails: Prisma.BatchPayload;
deleteFileUploadDetails: Prisma.BatchPayload;
}
}
55 changes: 55 additions & 0 deletions apps/issuance/libs/helpers/attributes.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { BadRequestException } from '@nestjs/common';
import { IIssuanceAttributes, ISchemaAttributes } from 'apps/issuance/interfaces/issuance.interfaces';

export function validateW3CSchemaAttributes(
filteredIssuanceAttributes: IIssuanceAttributes,
schemaUrlAttributes: ISchemaAttributes[]
): void {
const mismatchedAttributes: string[] = [];
const missingAttributes: string[] = [];
const extraAttributes: string[] = [];

const schemaAttributesSet = new Set(schemaUrlAttributes.map((attr) => attr.attributeName));

for (const schemaAttribute of schemaUrlAttributes) {
const { attributeName, schemaDataType, isRequired } = schemaAttribute;
const attributeValue = filteredIssuanceAttributes[attributeName];

if (isRequired && attributeValue === undefined) {
missingAttributes.push(`Attribute ${attributeName} is missing`);
continue;
}

if (isRequired && !attributeValue) {
mismatchedAttributes.push(`Attribute ${attributeName} must have a non-empty value`);
continue;
}

if (attributeValue !== undefined) {
const actualType = typeof attributeValue;
if (actualType !== schemaDataType) {
mismatchedAttributes.push(
`Attribute ${attributeName} has type ${actualType} but expected type ${schemaDataType}`
);
}
}
}

for (const attributeName in filteredIssuanceAttributes) {
if (!schemaAttributesSet.has(attributeName)) {
extraAttributes.push(`Attribute ${attributeName} is not defined in the schema`);
}
}

if (0 < missingAttributes.length) {
throw new BadRequestException(`Validation failed: ${missingAttributes.join(', ')}`);
}

if (0 < mismatchedAttributes.length) {
throw new BadRequestException(`Validation failed: ${mismatchedAttributes.join(', ')}`);
}

if (0 < extraAttributes.length) {
throw new BadRequestException(`Validation failed: ${extraAttributes.join(', ')}`);
}
}
15 changes: 15 additions & 0 deletions apps/issuance/src/issuance.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ export class IssuanceRepository {
}
}

async getSchemaDetails(schemaId: string): Promise<schema> {
try {
const schemaDetails = await this.prisma.schema.findFirstOrThrow({
where: {
schemaLedgerId: schemaId
}
});

return schemaDetails;
} catch (error) {
this.logger.error(`Error in get schema details: ${error.message}`);
throw new InternalServerErrorException(error.message);
}
}

async getCredentialDefinitionDetails(credentialDefinitionId: string): Promise<SchemaDetails> {
try {
const credentialDefinitionDetails = await this.prisma.credential_definition.findFirst({
Expand Down
69 changes: 60 additions & 9 deletions apps/issuance/src/issuance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CommonConstants } from '@credebl/common/common.constant';
import { ResponseMessages } from '@credebl/common/response-messages';
import { ClientProxy, RpcException } from '@nestjs/microservices';
import { map } from 'rxjs';
import { CredentialOffer, FileUpload, FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, ICredentialPayload, IIssuance, IIssueData, IPattern, IQueuePayload, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer, TemplateDetailsInterface } from '../interfaces/issuance.interfaces';
import { CredentialOffer, FileUpload, FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, ICredentialPayload, IIssuance, IIssueData, IPattern, IQueuePayload, ISchemaAttributes, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer, TemplateDetailsInterface } from '../interfaces/issuance.interfaces';
import { IssuanceProcessState, OrgAgentType, PromiseResult, SchemaType, TemplateIdentifier } from '@credebl/enum/enum';
import * as QRCode from 'qrcode';
import { OutOfBandIssuance } from '../templates/out-of-band-issuance.template';
Expand All @@ -33,6 +33,7 @@ import { RecordType, agent_invitations, organisation, user } from '@prisma/clien
import { createOobJsonldIssuancePayload, validateEmail } from '@credebl/common/cast.helper';
import { sendEmail } from '@credebl/common/send-grid-helper-file';
import { UserActivityRepository } from 'libs/user-activity/repositories';
import { validateW3CSchemaAttributes } from '../libs/helpers/attributes.validator';

@Injectable()
export class IssuanceService {
Expand Down Expand Up @@ -61,6 +62,21 @@ export class IssuanceService {
}
}

async getW3CSchemaAttributes(schemaUrl: string): Promise<ISchemaAttributes[]> {
const schemaRequest = await this.commonService.httpGet(schemaUrl).then(async (response) => response);
if (!schemaRequest) {
throw new NotFoundException(ResponseMessages.schema.error.W3CSchemaNotFOund, {
cause: new Error(),
description: ResponseMessages.errorMessages.notFound
});
}

const getSchemaDetails = await this.issuanceRepository.getSchemaDetails(schemaUrl);
const schemaAttributes = JSON.parse(getSchemaDetails?.attributes);

return schemaAttributes;
}

async sendCredentialCreateOffer(payload: IIssuance): Promise<ICredentialOfferResponse> {
try {
const { orgId, credentialDefinitionId, comment, credentialData } = payload || {};
Expand Down Expand Up @@ -136,6 +152,15 @@ export class IssuanceService {
autoAcceptCredential: payload.autoAcceptCredential || 'always',
comment: comment || ''
};
const payloadAttributes = issueData?.credentialFormats?.jsonld?.credential?.credentialSubject;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...filteredIssuanceAttributes } = payloadAttributes;

const schemaServerUrl = issueData?.credentialFormats?.jsonld?.credential?.['@context']?.[1];

const schemaUrlAttributes = await this.getW3CSchemaAttributes(schemaServerUrl);
validateW3CSchemaAttributes(filteredIssuanceAttributes, schemaUrlAttributes);
}

await this.delay(500);
Expand All @@ -147,9 +172,9 @@ export class IssuanceService {
const processedResults = results.map((result) => {
if (PromiseResult.REJECTED === result.status) {
return {
statusCode: result?.reason?.status?.message?.statusCode,
message: result?.reason?.status?.message?.error?.message,
error: ResponseMessages.errorMessages.serverError
statusCode: result?.reason?.status?.message?.statusCode || result?.reason?.response?.statusCode,
message: result?.reason?.status?.message?.error?.message || result?.reason?.response?.message,
error: result?.reason?.response?.error || ResponseMessages.errorMessages.serverError
};
} else if (PromiseResult.FULFILLED === result.status) {
return {
Expand Down Expand Up @@ -299,6 +324,16 @@ export class IssuanceService {
comment: comment || '',
invitationDid:invitationDid || undefined
};
const payloadAttributes = issueData?.credentialFormats?.jsonld?.credential?.credentialSubject;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...filteredIssuanceAttributes } = payloadAttributes;

const schemaServerUrl = issueData?.credentialFormats?.jsonld?.credential?.['@context']?.[1];

const schemaUrlAttributes = await this.getW3CSchemaAttributes(schemaServerUrl);
validateW3CSchemaAttributes(filteredIssuanceAttributes, schemaUrlAttributes);

}
const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(issueData, url, orgId);
if (isShortenUrl) {
Expand All @@ -316,7 +351,7 @@ export class IssuanceService {
const errorStack = error?.status?.message?.error;
if (errorStack) {
throw new RpcException({
message: errorStack?.reason ? errorStack?.reason : errorStack,
message: errorStack?.reason ? errorStack?.reason : errorStack?.message,
statusCode: error?.status?.code
});

Expand Down Expand Up @@ -716,6 +751,17 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO
label: organisation?.name,
imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl
};

const payloadAttributes = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.credentialSubject;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...filteredIssuanceAttributes } = payloadAttributes;

const schemaServerUrl = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.['@context']?.[1];

const schemaUrlAttributes = await this.getW3CSchemaAttributes(schemaServerUrl);
validateW3CSchemaAttributes(filteredIssuanceAttributes, schemaUrlAttributes);

}

const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, orgId);
Expand Down Expand Up @@ -768,13 +814,18 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO
if (errorStack) {
errors.push(
new RpcException({
error: `${errorStack?.error?.message} at position ${iterationNo}`,
statusCode: errorStack?.statusCode,
message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}`
message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}`,
error: `${errorStack?.error?.message} at position ${iterationNo}` })
);
} else {
errors.push(
new RpcException({
statusCode: error?.response?.statusCode,
message: `${error?.response?.message} at position ${iterationNo}`,
error: error?.response?.error
})
);
} else {
errors.push(new InternalServerErrorException(`${error.message} at position ${iterationNo}`));
}
return false;
}
Expand Down

0 comments on commit 4f53f4d

Please sign in to comment.