From bd5f171693a70385ba43d36996e7075f8c886317 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Sun, 28 Apr 2024 15:19:33 +0200 Subject: [PATCH] improve history management Signed-off-by: Mirko Mollik --- .../src/history/dto/history-response.dto.ts | 15 +++++ .../src/history/entities/history.entity.ts | 6 ++ .../backend/src/history/history.controller.ts | 6 +- apps/backend/src/history/history.service.ts | 51 +++++++++++++++-- .../src/oid4vc/oid4vp/oid4vp.service.ts | 7 ++- .../shared/api/kms/.openapi-generator/FILES | 2 + .../shared/api/kms/api/history.service.ts | 10 ++-- .../shared/api/kms/model/disclosed.ts | 18 ++++++ .../projects/shared/api/kms/model/history.ts | 2 + .../shared/api/kms/model/historyResponse.ts | 57 +++++++++++++++++++ .../projects/shared/api/kms/model/models.ts | 2 + .../history-list/history-list.component.html | 18 +++--- .../history-show/history-show.component.html | 15 +++++ .../history-show/history-show.component.ts | 4 +- 14 files changed, 188 insertions(+), 25 deletions(-) create mode 100644 apps/backend/src/history/dto/history-response.dto.ts create mode 100644 apps/holder/projects/shared/api/kms/model/disclosed.ts create mode 100644 apps/holder/projects/shared/api/kms/model/historyResponse.ts diff --git a/apps/backend/src/history/dto/history-response.dto.ts b/apps/backend/src/history/dto/history-response.dto.ts new file mode 100644 index 00000000..c58f0245 --- /dev/null +++ b/apps/backend/src/history/dto/history-response.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { History } from '../entities/history.entity'; + +class Disclosed { + key: string; + value: string; +} + +export class HistoryResponse extends History { + /** + * The values that were presented + */ + @ApiProperty({ type: [Disclosed] }) + disclosed: Disclosed[]; +} diff --git a/apps/backend/src/history/entities/history.entity.ts b/apps/backend/src/history/entities/history.entity.ts index 39ca2d28..5d007269 100644 --- a/apps/backend/src/history/entities/history.entity.ts +++ b/apps/backend/src/history/entities/history.entity.ts @@ -42,6 +42,12 @@ export class History { @Column() status: HistoryStatus; + @Column({ nullable: true }) + credentialType: string; + + @Column({ nullable: true }) + presentation: string; + /** * Date of creation */ diff --git a/apps/backend/src/history/history.controller.ts b/apps/backend/src/history/history.controller.ts index c10c8fdb..f6c3d8da 100644 --- a/apps/backend/src/history/history.controller.ts +++ b/apps/backend/src/history/history.controller.ts @@ -3,6 +3,7 @@ import { ApiOAuth2, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import { AuthGuard, AuthenticatedUser } from 'nest-keycloak-connect'; import { HistoryService } from './history.service'; import { KeycloakUser } from 'src/auth/user'; +import { HistoryResponse } from './dto/history-response.dto'; @UseGuards(AuthGuard) @ApiOAuth2([]) @@ -19,7 +20,10 @@ export class HistoryController { @ApiOperation({ summary: 'get one element' }) @Get(':id') - getOne(@AuthenticatedUser() user: KeycloakUser, @Param('id') id: string) { + getOne( + @AuthenticatedUser() user: KeycloakUser, + @Param('id') id: string + ): Promise { return this.historyService.getOne(id, user.sub); } diff --git a/apps/backend/src/history/history.service.ts b/apps/backend/src/history/history.service.ts index 68a9294c..04d89138 100644 --- a/apps/backend/src/history/history.service.ts +++ b/apps/backend/src/history/history.service.ts @@ -2,25 +2,54 @@ import { Injectable } from '@nestjs/common'; import { History } from './entities/history.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { CompactSdJwtVc } from '@sphereon/ssi-types'; +import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; +import { digest } from '@sd-jwt/crypto-nodejs'; +import { HistoryResponse } from './dto/history-response.dto'; @Injectable() export class HistoryService { + instance: SDJwtVcInstance; + constructor( @InjectRepository(History) private historyRepository: Repository - ) {} + ) { + this.instance = new SDJwtVcInstance({ hasher: digest }); + } all(user: string) { return this.historyRepository.find({ where: { user }, order: { created_at: 'DESC' }, // we only the id, relyingParty, created_at, and status fields to represent it as a list - select: ['id', 'relyingParty', 'created_at', 'status'], + select: [ + 'id', + 'relyingParty', + 'relyingPartyLogo', + 'credentialType', + 'created_at', + 'status', + ], }); } getOne(id: string, user: string) { - return this.historyRepository.findOne({ where: { id, user } }); + //TODO: decode the presentation to return the values that got presented + return this.historyRepository + .findOne({ where: { id, user } }) + .then(async (element: HistoryResponse) => { + if (element.presentation) { + const pres = await this.instance.decode(element.presentation); + element.disclosed = pres.disclosures.map((d) => ({ + key: d.key, + value: d.value as string, + })); + } + element.user = undefined; + element.presentation = undefined; + return element; + }); } add( @@ -40,8 +69,20 @@ export class HistoryService { return this.historyRepository.save(history); } - setStatus(id: string, status: 'accepted' | 'declined') { - return this.historyRepository.update({ id }, { status }); + async accept(id: string, presentation: CompactSdJwtVc) { + const pres = await this.instance.decode(presentation); + return this.historyRepository.update( + { id }, + { + presentation, + status: 'accepted', + credentialType: pres.jwt.payload.vct as string, + } + ); + } + + decline(id: string) { + return this.historyRepository.update({ id }, { status: 'declined' }); } delete(sub: string) { diff --git a/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts b/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts index f482a189..cf480a2a 100644 --- a/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts +++ b/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts @@ -22,6 +22,7 @@ import { SubmissionRequest } from './dto/submission-request.dto'; import { HistoryService } from 'src/history/history.service'; import { Oid4vpParseRequest } from './dto/parse-request.dto'; import { Session } from './session'; +import { CompactSdJwtVc } from '@sphereon/ssi-types'; @Injectable() export class Oid4vpService { @@ -199,7 +200,9 @@ export class Oid4vpService { const res = await session.op .submitAuthorizationResponse(authenticationResponseWithJWT) .catch(() => ''); - await this.historyService.setStatus(sessionId, 'accepted'); + const response = authenticationResponseWithJWT.response.payload + .vp_token as CompactSdJwtVc; + await this.historyService.accept(sessionId, response); this.sessions.delete(sessionId); } @@ -214,7 +217,7 @@ export class Oid4vpService { if (!session || session.user !== user) { throw new ConflictException('Session not found'); } - await this.historyService.setStatus(id, 'declined'); + await this.historyService.decline(id); this.sessions.delete(id); } diff --git a/apps/holder/projects/shared/api/kms/.openapi-generator/FILES b/apps/holder/projects/shared/api/kms/.openapi-generator/FILES index 151764da..51283e3d 100644 --- a/apps/holder/projects/shared/api/kms/.openapi-generator/FILES +++ b/apps/holder/projects/shared/api/kms/.openapi-generator/FILES @@ -17,7 +17,9 @@ model/createKey.ts model/cred.ts model/credentialIssuer.ts model/credentialResponse.ts +model/disclosed.ts model/history.ts +model/historyResponse.ts model/issuerMetadataLogo.ts model/keyResponse.ts model/keyType.ts diff --git a/apps/holder/projects/shared/api/kms/api/history.service.ts b/apps/holder/projects/shared/api/kms/api/history.service.ts index cdd35ff4..f12d03b7 100644 --- a/apps/holder/projects/shared/api/kms/api/history.service.ts +++ b/apps/holder/projects/shared/api/kms/api/history.service.ts @@ -20,6 +20,8 @@ import { Observable } from 'rxjs'; // @ts-ignore import { History } from '../model/history'; +// @ts-ignore +import { HistoryResponse } from '../model/historyResponse'; // @ts-ignore import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; @@ -230,9 +232,9 @@ export class HistoryApiService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public historyControllerGetOne(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public historyControllerGetOne(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public historyControllerGetOne(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public historyControllerGetOne(id: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public historyControllerGetOne(id: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public historyControllerGetOne(id: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; public historyControllerGetOne(id: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (id === null || id === undefined) { throw new Error('Required parameter id was null or undefined when calling historyControllerGetOne.'); @@ -282,7 +284,7 @@ export class HistoryApiService { } let localVarPath = `/history/${this.configuration.encodeParam({name: "id", value: id, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, diff --git a/apps/holder/projects/shared/api/kms/model/disclosed.ts b/apps/holder/projects/shared/api/kms/model/disclosed.ts new file mode 100644 index 00000000..acc28a95 --- /dev/null +++ b/apps/holder/projects/shared/api/kms/model/disclosed.ts @@ -0,0 +1,18 @@ +/** + * API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface Disclosed { + key: string; + value: string; +} + diff --git a/apps/holder/projects/shared/api/kms/model/history.ts b/apps/holder/projects/shared/api/kms/model/history.ts index 7c64855c..b8d73556 100644 --- a/apps/holder/projects/shared/api/kms/model/history.ts +++ b/apps/holder/projects/shared/api/kms/model/history.ts @@ -33,6 +33,8 @@ export interface History { */ relyingParty: string; relyingPartyLogo: string; + credentialType: string; + presentation: string; /** * Date of creation */ diff --git a/apps/holder/projects/shared/api/kms/model/historyResponse.ts b/apps/holder/projects/shared/api/kms/model/historyResponse.ts new file mode 100644 index 00000000..a7aebdaf --- /dev/null +++ b/apps/holder/projects/shared/api/kms/model/historyResponse.ts @@ -0,0 +1,57 @@ +/** + * API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Disclosed } from './disclosed'; + + +export interface HistoryResponse { + /** + * Status of the history entry + */ + status: HistoryResponse.StatusEnum; + /** + * The values that were presented + */ + disclosed: Array; + /** + * Unique ID of the history entry + */ + id: string; + /** + * The user that owns the key + */ + user: string; + /** + * Values + */ + value: string; + /** + * Relying party + */ + relyingParty: string; + relyingPartyLogo: string; + credentialType: string; + presentation: string; + /** + * Date of creation + */ + created_at: string; +} +export namespace HistoryResponse { + export type StatusEnum = 'pending' | 'accepted' | 'declined'; + export const StatusEnum = { + pending: 'pending' as StatusEnum, + accepted: 'accepted' as StatusEnum, + declined: 'declined' as StatusEnum + }; +} + + diff --git a/apps/holder/projects/shared/api/kms/model/models.ts b/apps/holder/projects/shared/api/kms/model/models.ts index 32c85996..0c0c05ee 100644 --- a/apps/holder/projects/shared/api/kms/model/models.ts +++ b/apps/holder/projects/shared/api/kms/model/models.ts @@ -3,7 +3,9 @@ export * from './createKey'; export * from './cred'; export * from './credentialIssuer'; export * from './credentialResponse'; +export * from './disclosed'; export * from './history'; +export * from './historyResponse'; export * from './issuerMetadataLogo'; export * from './keyResponse'; export * from './keyType'; diff --git a/apps/holder/projects/shared/history/history-list/history-list.component.html b/apps/holder/projects/shared/history/history-list/history-list.component.html index 579dd7f2..6b890545 100644 --- a/apps/holder/projects/shared/history/history-list/history-list.component.html +++ b/apps/holder/projects/shared/history/history-list/history-list.component.html @@ -7,17 +7,13 @@

History

@for (history of values; track history) { - - call_made -

{{ history.relyingParty }}

-

{{ history.created_at | date: 'medium' }}

+ logo +

+ {{ history.credentialType }} +

+

Declined

+ {{ history.relyingParty }} + {{ history.created_at | date: 'medium' }}
}
diff --git a/apps/holder/projects/shared/history/history-show/history-show.component.html b/apps/holder/projects/shared/history/history-show/history-show.component.html index c6b78fa0..5fa41119 100644 --- a/apps/holder/projects/shared/history/history-show/history-show.component.html +++ b/apps/holder/projects/shared/history/history-show/history-show.component.html @@ -18,6 +18,21 @@

{{ element.relyingParty }}

Action
{{ element.status }}
+ + +
Credential
+
{{ element.credentialType }}
+
+
+ +
Presented values
+ @for (disclosed of element.disclosed; track disclosed) { + +
{{ disclosed.key }}
+
{{ disclosed.value }}
+
+ } +
diff --git a/apps/holder/projects/shared/history/history-show/history-show.component.ts b/apps/holder/projects/shared/history/history-show/history-show.component.ts index d70f5f00..e0697ec7 100644 --- a/apps/holder/projects/shared/history/history-show/history-show.component.ts +++ b/apps/holder/projects/shared/history/history-show/history-show.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { History, HistoryApiService } from '../../api/kms'; +import { History, HistoryApiService, HistoryResponse } from '../../api/kms'; import { firstValueFrom } from 'rxjs'; import { MatCardModule } from '@angular/material/card'; import { CommonModule } from '@angular/common'; @@ -25,7 +25,7 @@ import { MatListModule } from '@angular/material/list'; styleUrl: './history-show.component.scss', }) export class HistoryShowComponent implements OnInit { - element!: History; + element!: HistoryResponse; constructor( private route: ActivatedRoute,