diff --git a/apps/vc-api/docs/openapi.json b/apps/vc-api/docs/openapi.json index 9405114f..1e578487 100644 --- a/apps/vc-api/docs/openapi.json +++ b/apps/vc-api/docs/openapi.json @@ -840,6 +840,48 @@ }, "tags": ["vc-api"] } + }, + "/v1/vc-api/workflows/{localWorkflowId}/exchanges/{localExchangeId}/steps/{localStepId}/review": { + "post": { + "operationId": "VcApiController_addWorkflowStepReview", + "summary": "", + "description": "Update an exchange step review\nA NON-STANDARD endpoint currently.\n", + "parameters": [ + { "name": "localWorkflowId", "required": true, "in": "path", "schema": { "type": "string" } }, + { "name": "localExchangeId", "required": true, "in": "path", "schema": { "type": "string" } }, + { "name": "localStepId", "required": true, "in": "path", "schema": { "type": "string" } } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/SubmissionReviewDto" } } + } + }, + "responses": { + "201": { "description": "" }, + "400": { + "description": "", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestErrorResponseDto" } } + } + }, + "404": { + "description": "", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundErrorResponseDto" } } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/InternalServerErrorResponseDto" } + } + } + } + }, + "tags": ["vc-api"] + } } }, "info": { "title": "VC-API", "description": "Sample VC-API", "version": "0.1", "contact": {} }, diff --git a/apps/vc-api/src/vc-api/vc-api.controller.ts b/apps/vc-api/src/vc-api/vc-api.controller.ts index e6f0c199..b7c5f795 100644 --- a/apps/vc-api/src/vc-api/vc-api.controller.ts +++ b/apps/vc-api/src/vc-api/vc-api.controller.ts @@ -514,4 +514,31 @@ export class VcApiController { ): Promise { return this.workflowService.getExchangeStep(localWorkflowId, localExchangeId, localStepId); } + + /** + * Add review for an exchange step + * + * @param localWorkflowId + * @param localExchangeId + * @param localStepId + * @param submissionReview + */ + @Post('/workflows/:localWorkflowId/exchanges/:localExchangeId/steps/:localStepId/review') + @ApiOperation({ + description: + 'Update an exchange step review\n' + + 'A NON-STANDARD endpoint currently.\n' + }) + @ApiBody({ type: SubmissionReviewDto }) + @ApiCreatedResponse() + @ApiNotFoundResponse({ type: NotFoundErrorResponseDto }) + @ApiNotFoundResponse({ type: BadRequestErrorResponseDto }) + async addWorkflowStepReview( + @Param('localWorkflowId') localWorkflowId: string, + @Param('localExchangeId') localExchangeId: string, + @Param('localStepId') localStepId: string, + @Body() submissionReview: SubmissionReviewDto + ) { + await this.workflowService.addReview(localWorkflowId, localExchangeId, localStepId, submissionReview); + } } diff --git a/apps/vc-api/src/vc-api/workflows/dtos/presentation-submission-full.dto.ts b/apps/vc-api/src/vc-api/workflows/dtos/presentation-submission-full.dto.ts index 772a3493..b6d8115e 100644 --- a/apps/vc-api/src/vc-api/workflows/dtos/presentation-submission-full.dto.ts +++ b/apps/vc-api/src/vc-api/workflows/dtos/presentation-submission-full.dto.ts @@ -27,5 +27,5 @@ export class PresentationSubmissionFullDto { @IsNotEmpty() @ValidateNested() @Type(() => VerifiablePresentationDto) - vp: VerifiablePresentationDto; + verifiablePresentation: VerifiablePresentationDto; } diff --git a/apps/vc-api/src/vc-api/workflows/dtos/submission-review.dto.ts b/apps/vc-api/src/vc-api/workflows/dtos/submission-review.dto.ts new file mode 100644 index 00000000..f9763776 --- /dev/null +++ b/apps/vc-api/src/vc-api/workflows/dtos/submission-review.dto.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021 - 2023 Energy Web Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from 'class-transformer'; +import { IsEnum, IsOptional, ValidateNested } from 'class-validator'; +import { VerifiablePresentationDto } from '../../credentials/dtos/verifiable-presentation.dto'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export enum ReviewResult { + approved = 'approved', + rejected = 'rejected' +} + +export class SubmissionReviewDto { + @IsEnum(ReviewResult) + @ApiProperty({ + description: 'The judgement made by the reviewer', + enum: ReviewResult, + enumName: 'ReviewResult' + }) + result: ReviewResult; + + @ValidateNested() + @IsOptional() + @Type(() => VerifiablePresentationDto) + @ApiPropertyOptional({ + description: 'A reviewer may want to include credentials (wrapped in a VP) to the holder' + }) + vp?: VerifiablePresentationDto; +} diff --git a/apps/vc-api/src/vc-api/workflows/workflow.service.ts b/apps/vc-api/src/vc-api/workflows/workflow.service.ts index 202e80d8..68fe31c4 100644 --- a/apps/vc-api/src/vc-api/workflows/workflow.service.ts +++ b/apps/vc-api/src/vc-api/workflows/workflow.service.ts @@ -17,6 +17,8 @@ import { HttpService } from '@nestjs/axios'; import { validate } from 'class-validator'; import { ExchangeStepStateDto } from './dtos/exchange-step-state.dto'; import { API_DEFAULT_VERSION_PREFIX } from '../../setup'; +import { SubmissionReviewDto } from './dtos/submission-review.dto'; +import { IssuanceExchangeStep } from './types/issuance-exchange-step'; export class WorkflowService { private readonly logger = new Logger(WorkflowService.name, { timestamp: true }); @@ -193,4 +195,29 @@ export class WorkflowService { stepResponse: response }; } + + public async addReview( + localWorkflowId: string, + localExchangeId: string, + localStepId: string, + reviewDto: SubmissionReviewDto) { + const exchange = await this.exchangeRepository.findOneBy({ + workflowId: localWorkflowId, + exchangeId: localExchangeId + }); + if (exchange == null) { + throw new NotFoundException(`workflowId='${localWorkflowId} -> 'exchangeId='${localExchangeId}' does not exist`); + } + + const stepInfo = exchange.getStep(localStepId); + + if (stepInfo instanceof IssuanceExchangeStep) { + stepInfo.addVP(reviewDto.vp); + } else { + throw new BadRequestException('Only Issuance exchange step is supported'); + } + const id = exchange.steps.findIndex((step) => step.stepId === localStepId); + exchange.steps[id] = stepInfo; + await this.exchangeRepository.save(exchange); + } } diff --git a/apps/vc-api/test/vc-api/workflows/resident-card/resident-card-presentation.workflow.ts b/apps/vc-api/test/vc-api/workflows/resident-card/resident-card-presentation.workflow.ts index 1b8b042b..87ab8f8a 100644 --- a/apps/vc-api/test/vc-api/workflows/resident-card/resident-card-presentation.workflow.ts +++ b/apps/vc-api/test/vc-api/workflows/resident-card/resident-card-presentation.workflow.ts @@ -11,7 +11,7 @@ import { VpRequestQueryType } from '../../../../src/vc-api/exchanges/types/vp-re export class ResidentCardPresentation { #workflowId = `b229a18f-db45-4b33-8d36-25d442467bab`; #callbackUrl: string; - queryType = VpRequestQueryType.presentationDefinition; + queryType = VpRequestQueryType.didAuth; constructor(callbackUrl: string) { this.#callbackUrl = callbackUrl; diff --git a/apps/vc-api/test/vc-api/workflows/resident-card/resident-card.e2e-suite.ts b/apps/vc-api/test/vc-api/workflows/resident-card/resident-card.e2e-suite.ts index 92f2b66c..29e515c4 100644 --- a/apps/vc-api/test/vc-api/workflows/resident-card/resident-card.e2e-suite.ts +++ b/apps/vc-api/test/vc-api/workflows/resident-card/resident-card.e2e-suite.ts @@ -147,7 +147,7 @@ export const residentCardWorkflowSuite = () => { // As holder, start issuance exchange // POST /workflows/{localWorkflowId}/exchanges/{localExchangeId} const presentationExchangeEndpoint = getUrlPath(presentationExchangeId); - const presentationVpRequest = await walletClient.startExchange( + const presentationVpRequest = await walletClient.startWorkflowExchange( presentationExchangeEndpoint, presentationWorkflow.queryType ); @@ -174,7 +174,7 @@ export const residentCardWorkflowSuite = () => { const vp = await walletClient.provePresentation({ presentation, options: presentationOptions }); // Holder submits presentation - await walletClient.continueWorkflowExchange(presentationExchangeContinuationEndpoint, vp, 'empty'); + await walletClient.continueWorkflowExchange(presentationExchangeContinuationEndpoint, vp, 'redirectUrl'); presentationCallbackScope.done(); }); }; diff --git a/apps/vc-api/test/wallet-client.ts b/apps/vc-api/test/wallet-client.ts index e856464b..a006b533 100644 --- a/apps/vc-api/test/wallet-client.ts +++ b/apps/vc-api/test/wallet-client.ts @@ -28,7 +28,7 @@ const EXPECTED_RESPONSE_TYPE = { VpRequest: 'vpRequest', RedirectUrl: 'redirectUrl', VerifiablePresentation: 'verifiablePresentation', - Empty: 'empty' // An empty response isn't clearly in the spec but there may be a use for it + Empty: 'empty', // An empty response isn't clearly in the spec but there may be a use for it, } as const; type ExpectedResponseType = (typeof EXPECTED_RESPONSE_TYPE)[keyof typeof EXPECTED_RESPONSE_TYPE];