diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7cdbce40..acbbe78d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,9 +41,13 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # build the docker image for keycloak + # build the docker image for keycloak and publish it to the GitHub Container Registry - name: Build Keycloak Docker Image - run: cd deploys/keycloak && docker compose build keycloak && docker compose push keycloak + run: | + cd deploys/keycloak && + docker compose pull keycloak && + docker compose build keycloak && + docker compose push keycloak # add the release, build the container and release it with the information for sentry - name: Build and push images diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eba7d0bc..3b4a17c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,12 +42,15 @@ jobs: run: npx playwright install --with-deps - name: Build keycloak - run: cd deploys/keycloak && docker compose build keycloak + run: | + cd deploys/keycloak && + docker compose pull keycloak && + docker compose build keycloak - name: Add entry to /etc/hosts run: echo "127.0.0.1 host.testcontainers.internal" | sudo tee -a /etc/hosts - - name: Lint, test, build, e2e + - name: Lint, test, container, e2e run: INPUT_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} pnpm exec nx affected -t lint test container e2e # comment out since the current e2e tests do not produce any artifacts # - name: Upload coverage @@ -76,6 +79,12 @@ jobs: path: tmp/logs retention-days: 30 + # validate if the .env.example files in the deploys folder are up to date and that the containers can be health checked and started + - name: Validate deploy environment + run: + cd deploys && + ./test.sh + - name: Check if testcontainer logs exist id: check_testcontainer_logs run: echo "exists=$(if [ -d tmp/logs ]; then echo true; else echo false; fi)" >> $GITHUB_ENV diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 00000000..d2a00c43 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,2 @@ +# Exclusions +sonar.exclusions=libs/verifier-shared/src/lib/api/** libs/issuer-shared/src/lib/api/** libs/holder-shared/src/lib/api/** diff --git a/README.md b/README.md index 956521bc..2b8b0843 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ +[![CD](https://github.com/openwallet-foundation-labs/credhub/actions/workflows/cd.yml/badge.svg)](https://github.com/openwallet-foundation-labs/credhub/actions/workflows/cd.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=openwallet-foundation-labs_credhub&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=openwallet-foundation-labs_credhub) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/openwallet-foundation/credo-ts/main/LICENSE) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/) + # credhub credhub is comprehensive monorepo including a cloud wallet for natural persons together with a minimal issuer and verifier service. The cloud wallet will host all credentials and key pairs, including the business logic to receive and present credentials. # Getting Started + Documentation on how to get started with credhub can be found at [https://credhub.eu](https://credhub.eu) # Virtual meetings + There is a bi weekly virtual meeting to discuss the progress of the project. You can get a calendar invite [here](https://zoom-lfx.platform.linuxfoundation.org/meeting/93045942637?password=2c738e22-bb7b-44a7-aab1-e98fa7fc82f6) # Contributing + If you would like to contribute to the project, please read our [contributing guide](./CONTRIBUTING.md). # License + This project is licensed under the [Apache License Version 2.0 (Apache-2.0).](./LICENSE) diff --git a/apps/demo/Dockerfile b/apps/demo/Dockerfile index d4b64c50..6aa891d1 100644 --- a/apps/demo/Dockerfile +++ b/apps/demo/Dockerfile @@ -1,4 +1,8 @@ FROM docker.io/nginx:stable-alpine + +# Install wget +RUN apk --no-cache add wget + COPY dist/apps/demo/* /usr/share/nginx/html/ RUN echo "server {" > /etc/nginx/conf.d/default.conf && \ echo " listen 80;" >> /etc/nginx/conf.d/default.conf && \ diff --git a/apps/holder-app-e2e/src/credentials.spec.ts b/apps/holder-app-e2e/src/credentials.spec.ts index 31407fb6..10c8e527 100644 --- a/apps/holder-app-e2e/src/credentials.spec.ts +++ b/apps/holder-app-e2e/src/credentials.spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { test, expect, Page } from '@playwright/test'; import { getConfig, register } from './helpers'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { GlobalConfig } from '../global-setup'; export const username = faker.internet.email(); @@ -48,17 +48,24 @@ async function getAxiosInstance(port: number) { async function receiveCredential(pin = false) { const axios = await getAxiosInstance(config.issuerPort); + const templates = await axios + .get('/templates') + .then((response) => response.data); + const credentialId = templates.find( + (template: any) => template.name === 'Identity' + ).id; + const response = await axios .post(`/sessions`, { credentialSubject: { prename: 'Max', surname: 'Mustermann', }, - credentialId: 'Identity', + credentialId, pin, }) - .catch((e) => { - console.log(e); + .catch((e: AxiosError) => { + console.log(JSON.stringify(e.response?.data, null, 2)); throw Error('Failed to create session'); }); const uri = response.data.uri; @@ -98,8 +105,13 @@ test('issuance with pin', async () => { test('verify credential', async () => { await receiveCredential(); - const credentialId = 'Identity'; const axios = await getAxiosInstance(config.verifierPort); + const templates = await axios + .get('/templates') + .then((response) => response.data); + const credentialId = templates.find( + (template: any) => template.value.name === 'Identity' + ).id; let uri = ''; try { const response = await axios.post(`/siop/${credentialId}`); diff --git a/apps/holder-app/Dockerfile b/apps/holder-app/Dockerfile index 8b2bafec..29e71cfb 100644 --- a/apps/holder-app/Dockerfile +++ b/apps/holder-app/Dockerfile @@ -1,5 +1,8 @@ FROM docker.io/nginx:stable-alpine +# Install wget +RUN apk --no-cache add wget + # Copy application files and the startup script with permissions COPY dist/apps/holder-app/* /usr/share/nginx/html/ COPY --chmod=755 apps/holder-app/startup.sh /usr/local/bin/startup.sh diff --git a/apps/holder-app/src/app/app.config.ts b/apps/holder-app/src/app/app.config.ts index 7e82248e..44d85ebd 100644 --- a/apps/holder-app/src/app/app.config.ts +++ b/apps/holder-app/src/app/app.config.ts @@ -5,7 +5,12 @@ import { isDevMode, ErrorHandler, } from '@angular/core'; -import { Router, provideRouter } from '@angular/router'; +import { + RouteReuseStrategy, + Router, + provideRouter, + withRouterConfig, +} from '@angular/router'; import { routes } from './app.routes'; import { @@ -27,6 +32,24 @@ import { provideServiceWorker } from '@angular/service-worker'; import * as Sentry from '@sentry/angular'; import { environment } from '../environments/environment'; +class MyStrategy implements RouteReuseStrategy { + shouldDetach() { + return false; + } + store() { + return null; + } + shouldAttach() { + return false; + } + retrieve() { + return null; + } + shouldReuseRoute() { + return false; + } +} + Sentry.init({ dsn: environment.sentryDsn, enabled: !isDevMode(), @@ -45,11 +68,12 @@ Sentry.init({ export const appConfig: ApplicationConfig = { providers: [ - provideRouter(routes), + provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })), provideAnimations(), provideOAuthClient(), provideHttpClient(withInterceptorsFromDi()), importProvidersFrom(ApiModule), + { provide: RouteReuseStrategy, useClass: MyStrategy }, { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { hasBackdrop: true } }, { provide: APP_INITIALIZER, diff --git a/apps/holder-backend/.env.example b/apps/holder-backend/.env.example index c5c2448b..5c736b02 100644 --- a/apps/holder-backend/.env.example +++ b/apps/holder-backend/.env.example @@ -3,7 +3,7 @@ OIDC_AUTH_URL=http://host.docker.internal:8080 OIDC_REALM=wallet OIDC_PUBLIC_CLIENT_ID=wallet OIDC_ADMIN_CLIENT_ID=wallet-admin -OIDC_ADMIN_CLIENT_SECRET=secret +OIDC_ADMIN_CLIENT_SECRET=kwpCrguxUOn9gump77E0B3vAkiOhW8eL # DB config # DB_TYPE=postgres diff --git a/apps/holder-backend/src/app/credentials/credentials.controller.ts b/apps/holder-backend/src/app/credentials/credentials.controller.ts index 054532fc..a65f8d56 100644 --- a/apps/holder-backend/src/app/credentials/credentials.controller.ts +++ b/apps/holder-backend/src/app/credentials/credentials.controller.ts @@ -1,24 +1,15 @@ import { - Body, ConflictException, Controller, Delete, Get, Param, - Post, Query, UseGuards, } from '@nestjs/common'; -import { - ApiBody, - ApiOAuth2, - ApiOperation, - ApiQuery, - ApiTags, -} from '@nestjs/swagger'; +import { ApiOAuth2, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { AuthGuard, AuthenticatedUser } from 'nest-keycloak-connect'; import { CredentialsService } from './credentials.service'; -import { CreateCredentialDto } from './dto/create-credential.dto'; import { CredentialResponse } from './dto/credential-response.dto'; import { KeycloakUser } from '../auth/user'; @@ -29,16 +20,6 @@ import { KeycloakUser } from '../auth/user'; export class CredentialsController { constructor(private readonly credentialsService: CredentialsService) {} - @ApiOperation({ summary: 'store a credential' }) - @ApiBody({ type: CreateCredentialDto }) - @Post() - create( - @Body() createCredentialDto: CreateCredentialDto, - @AuthenticatedUser() user: KeycloakUser - ) { - return this.credentialsService.create(createCredentialDto, user.sub); - } - @ApiOperation({ summary: 'get all credentials' }) @ApiQuery({ name: 'archive', required: false, type: Boolean }) @Get() @@ -52,7 +33,7 @@ export class CredentialsController { ); return credentials.map((credential) => ({ id: credential.id, - display: credential.metaData.display[0], + display: credential.metaData.display?.[0], issuer: credential.issuer, })); } diff --git a/apps/holder-backend/src/app/credentials/credentials.service.ts b/apps/holder-backend/src/app/credentials/credentials.service.ts index ec3024d7..fc262045 100644 --- a/apps/holder-backend/src/app/credentials/credentials.service.ts +++ b/apps/holder-backend/src/app/credentials/credentials.service.ts @@ -22,7 +22,6 @@ import { firstValueFrom } from 'rxjs'; import { Verifier } from '@sd-jwt/types'; import { JWK, JWTPayload } from '@sphereon/oid4vci-common'; import { CryptoService, ResolverService } from '@credhub/backend'; -import { getListFromStatusListJWT } from '@sd-jwt/jwt-status-list'; type DateKey = 'exp' | 'nbf'; @Injectable() @@ -119,7 +118,7 @@ export class CredentialsService { return this.instance .decode(credential) .then((vc) => - vc.jwt.payload[key] ? (vc.jwt.payload[key] as number) : undefined + vc.jwt.payload[key] ? (vc.jwt.payload[key] as number) * 1000 : undefined ); } diff --git a/apps/holder-backend/src/app/oid4vc/oid4vci/oid4vci.service.ts b/apps/holder-backend/src/app/oid4vc/oid4vci/oid4vci.service.ts index 3e3c4087..c904d300 100644 --- a/apps/holder-backend/src/app/oid4vc/oid4vci/oid4vci.service.ts +++ b/apps/holder-backend/src/app/oid4vc/oid4vci/oid4vci.service.ts @@ -136,7 +136,6 @@ export class Oid4vciService { const sdjwtvc = await this.sdjwt.decode( credentialResponse.credential as string ); - //TODO: also save the reference to the credential metadata. This will allow use to render the credential later. Either save the metadata or save a reference so it can be loaded on demand. const credentialEntry = await this.credentialsService.create( { value: credentialResponse.credential as string, diff --git a/apps/issuer-backend/src/app/issuer/issuer-data.service.ts b/apps/issuer-backend/src/app/issuer/issuer-data.service.ts index 5fcc249e..70b543ac 100644 --- a/apps/issuer-backend/src/app/issuer/issuer-data.service.ts +++ b/apps/issuer-backend/src/app/issuer/issuer-data.service.ts @@ -30,7 +30,7 @@ export class IssuerDataService { this.metadata.credential_issuer = this.configSerivce.get('ISSUER_BASE_URL'); this.metadata.credential_configurations_supported = - this.templatesService.getSupported(await this.templatesService.listAll()); + await this.templatesService.getSupported(); } /** @@ -51,18 +51,23 @@ export class IssuerDataService { /** * Returns the disclosure frame of the credential with the given id, throws an error if the credential is not supported. - * @param id + * @param vct * @returns */ - async getDisclosureFrame(id: string) { + async getDisclosureFrame(vct: string) { if (this.configSerivce.get('CONFIG_RELOAD')) { this.loadConfig(); } - const credential = await this.templatesService.getOne(id); + //becuase the vct is stored in a json field, we will need to fetch all elements and then filter + const credential = await this.templatesService + .listAll() + .then((templates) => + templates.find((template) => template.value.schema.vct === vct) + ); if (!credential) { - throw new Error(`The credential with the id ${id} is not supported.`); + throw new Error(`The credential with the id ${vct} is not supported.`); } - return credential.sd; + return credential.value.sd; } /** diff --git a/apps/issuer-backend/src/app/issuer/issuer.controller.ts b/apps/issuer-backend/src/app/issuer/issuer.controller.ts index e27c8da3..ff9a135a 100644 --- a/apps/issuer-backend/src/app/issuer/issuer.controller.ts +++ b/apps/issuer-backend/src/app/issuer/issuer.controller.ts @@ -6,17 +6,19 @@ import { NotFoundException, Param, Post, + Query, UseGuards, } from '@nestjs/common'; import { IssuerService } from './issuer.service'; import { SessionRequestDto } from './dto/session-request.dto'; -import { ApiOAuth2, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOAuth2, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'nest-keycloak-connect'; import { SessionResponseDto } from './dto/session-response.dto'; import { CredentialOfferSession } from './dto/credential-offer-session.dto'; import { DBStates } from '@credhub/relying-party-shared'; import { CredentialsService } from '../credentials/credentials.service'; import { SessionEntryDto } from './dto/session-entry.dto'; +import { CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common'; @UseGuards(AuthGuard) @ApiOAuth2([]) @@ -29,12 +31,27 @@ export class IssuerController { ) {} @ApiOperation({ summary: 'Lists all sessions' }) + @ApiQuery({ name: 'configId', required: false }) @Get() - async listAll(): Promise { + async listAll( + @Query('configId') configId?: string + ): Promise { return ( this.issuerService.vcIssuer .credentialOfferSessions as DBStates - ).all(); + ) + .all() + .then((entries) => { + if (configId) { + return entries.filter((entry) => + ( + entry.credentialOffer + .credential_offer as CredentialOfferPayloadV1_0_13 + ).credential_configuration_ids.includes(configId) + ); + } + return entries; + }); } @ApiOperation({ summary: 'Returns the status for a session' }) @@ -50,7 +67,7 @@ export class IssuerController { const credentials = await this.credentialsService.getBySessionId(id); return { session, - credentials: credentials, + credentials, }; } diff --git a/apps/issuer-backend/src/app/issuer/issuer.service.ts b/apps/issuer-backend/src/app/issuer/issuer.service.ts index 2cba96ca..7d74aaf8 100644 --- a/apps/issuer-backend/src/app/issuer/issuer.service.ts +++ b/apps/issuer-backend/src/app/issuer/issuer.service.ts @@ -97,12 +97,14 @@ export class IssuerService { let exp: number | undefined; // we either use the passed exp value or the ttl of the credential. If none is set, the credential will not expire. if (values.exp) { + //TODO: make sure that the exp is in seconds exp = values.exp; - } else if (credential.ttl) { + } else if (credential.value.ttl) { const expDate = new Date(); - expDate.setSeconds(expDate.getSeconds() + credential.ttl); + expDate.setSeconds(expDate.getSeconds() + credential.value.ttl); exp = expDate.getTime(); } + exp = exp > 1e10 ? Math.floor(exp / 1000) : exp; const credentialDataSupplierInput: CredentialDataSupplierInput = { credentialSubject: values.credentialSubject, @@ -118,7 +120,7 @@ export class IssuerService { } const response = await this.vcIssuer.createCredentialOfferURI({ - credential_configuration_ids: [credential.schema.id as string], + credential_configuration_ids: [credential.id], grants: { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { 'pre-authorized_code': sessionId, @@ -156,14 +158,9 @@ export class IssuerService { } async init() { - const verifier = await this.crypto.getVerifier( - await this.keyService.getPublicKey() - ); - // crearre the sd-jwt instance with the required parameters. const sdjwt = new SDJwtVcInstance({ signer: this.keyService.signer, - verifier, signAlg: this.crypto.alg, hasher: digest, hashAlg: 'SHA-256', @@ -293,6 +290,7 @@ export class IssuerService { accessTokenSignerCallback: signerCallback, accessTokenIssuer: this.configService.get('ISSUER_BASE_URL'), preAuthorizedCodeExpirationDuration: 1000 * 60 * 10, + //TODO: the expiration should be passed to the user so he knows when the token is not valid anymore and avoids using it. tokenExpiresIn: 300, }, //TODO: not implemented yet diff --git a/apps/issuer-backend/src/app/templates/dto/template.dto.ts b/apps/issuer-backend/src/app/templates/dto/template.dto.ts index c767fa8b..20411907 100644 --- a/apps/issuer-backend/src/app/templates/dto/template.dto.ts +++ b/apps/issuer-backend/src/app/templates/dto/template.dto.ts @@ -55,9 +55,6 @@ export class CredentialConfigurationSupportedV1_0_13 @IsOptional() vct: string; - @IsString() - id: string; - @IsOptional() @IsObject() claims?: IssuerCredentialSubject; @@ -89,13 +86,17 @@ export class CredentialConfigurationSupportedV1_0_13 } export class Template { + @IsString() + name: string; + @ValidateNested() @Type(() => CredentialConfigurationSupportedV1_0_13) schema: CredentialConfigurationSupportedV1_0_13; @IsObject() - sd: DisclosureFrame>; + sd: DisclosureFrame>; @IsInt() + @IsOptional() ttl: number; } diff --git a/apps/issuer-backend/src/app/templates/schemas/temoplate.entity.ts b/apps/issuer-backend/src/app/templates/schemas/temoplate.entity.ts index 15fc20e7..878e5a4f 100644 --- a/apps/issuer-backend/src/app/templates/schemas/temoplate.entity.ts +++ b/apps/issuer-backend/src/app/templates/schemas/temoplate.entity.ts @@ -1,11 +1,14 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Template as TemplateDTO } from '../dto/template.dto'; @Entity() -export class Template { - @PrimaryColumn() +export class TemplateEntity { + @PrimaryGeneratedColumn('uuid') id: string; + @Column() + name: string; + @Column({ type: 'json' }) value: TemplateDTO; } diff --git a/apps/issuer-backend/src/app/templates/template.service.ts b/apps/issuer-backend/src/app/templates/template.service.ts index 1344d4af..d3f43600 100644 --- a/apps/issuer-backend/src/app/templates/template.service.ts +++ b/apps/issuer-backend/src/app/templates/template.service.ts @@ -1,7 +1,7 @@ import { ConflictException, Injectable } from '@nestjs/common'; import { Template } from './dto/template.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Template as TemplateEntity } from './schemas/temoplate.entity'; +import { TemplateEntity as TemplateEntity } from './schemas/temoplate.entity'; import { Repository } from 'typeorm'; import { CredentialConfigurationSupportedV1_0_13 } from '@sphereon/oid4vci-common'; import { readdirSync, readFileSync } from 'fs'; @@ -9,6 +9,7 @@ import { join } from 'path'; import { ConfigService } from '@nestjs/config'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; +import { CryptoService } from '@credhub/backend'; @Injectable() export class TemplatesService { @@ -16,7 +17,8 @@ export class TemplatesService { constructor( @InjectRepository(TemplateEntity) private templateRepository: Repository, - private configSerivce: ConfigService + private configSerivce: ConfigService, + private cryptoService: CryptoService ) { this.folder = this.configSerivce.get('CREDENTIALS_FOLDER'); } @@ -37,41 +39,55 @@ export class TemplatesService { console.error(JSON.stringify(errors, null, 2)); } else { //check if an id is already used - await this.getOne(template.schema.id).catch(async () => { - await this.create(template); - }); + await this.templateRepository + .findOneByOrFail({ name: template.name }) + .catch(async () => { + await this.create(template); + }); } } } - getSupported(value: Map) { + async getSupported() { + const rec: Map = new Map(); + const elements = await this.templateRepository.find(); + elements.forEach((element) => rec.set(element.id, element.value)); + //iterate over the map and change the value const result: Record = {}; - value.forEach((v, k) => { + rec.forEach((v, k) => { + v.schema.credential_signing_alg_values_supported = + this.getSigningAlgValuesSupported(); + v.schema.cryptographic_binding_methods_supported = + this.getCryptographicBindingMethodsSupported(); result[k] = v.schema; }); return result; } + private getSigningAlgValuesSupported() { + // we assume that we are only using one algorithm when issuing credentials + return [this.cryptoService.getAlg()]; + } + + private getCryptographicBindingMethodsSupported() { + return ['jwk']; + } + async listAll() { - const rec: Map = new Map(); - const elements = await this.templateRepository.find(); - elements.forEach((element) => rec.set(element.id, element.value)); - return rec; + return this.templateRepository.find(); } async getOne(id: string) { - return this.templateRepository - .findOneByOrFail({ id }) - .then((res) => res.value); + return this.templateRepository.findOneByOrFail({ id }); } - async create(data: Template) { + async create(value: Template) { await this.templateRepository .save( this.templateRepository.create({ - id: data.schema.id, - value: data, + value, + name: value.name, }) ) .catch((err) => { @@ -80,10 +96,10 @@ export class TemplatesService { } async update(id: string, data: Template) { - if (id !== data.schema.id) { - throw new ConflictException('Id in path and in data do not match'); - } - await this.templateRepository.update({ id }, { value: data }); + await this.templateRepository.update( + { id }, + { value: data, name: data.name } + ); } async delete(id: string) { diff --git a/apps/issuer-backend/src/app/templates/templates.controller.ts b/apps/issuer-backend/src/app/templates/templates.controller.ts index d744fd33..17a108a7 100644 --- a/apps/issuer-backend/src/app/templates/templates.controller.ts +++ b/apps/issuer-backend/src/app/templates/templates.controller.ts @@ -12,6 +12,7 @@ import { ApiTags, ApiOAuth2, ApiOperation } from '@nestjs/swagger'; import { AuthGuard } from 'nest-keycloak-connect'; import { Template } from './dto/template.dto'; import { TemplatesService } from './template.service'; +import { TemplateEntity } from './schemas/temoplate.entity'; @ApiTags('templates') @UseGuards(AuthGuard) @@ -22,10 +23,8 @@ export class TemplatesController { @ApiOperation({ summary: 'List all templates' }) @Get() - async listAll(): Promise { - return Object.values( - Object.fromEntries(await this.templatesService.listAll()) - ); + async listAll(): Promise { + return this.templatesService.listAll(); } @ApiOperation({ summary: 'Get one template' }) diff --git a/apps/issuer-backend/src/app/templates/templates.module.ts b/apps/issuer-backend/src/app/templates/templates.module.ts index 22087fe2..a0d0a672 100644 --- a/apps/issuer-backend/src/app/templates/templates.module.ts +++ b/apps/issuer-backend/src/app/templates/templates.module.ts @@ -1,13 +1,13 @@ import { Module, OnModuleInit } from '@nestjs/common'; import { TemplatesController } from './templates.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Template } from './schemas/temoplate.entity'; +import { TemplateEntity } from './schemas/temoplate.entity'; import { TemplatesService } from './template.service'; import { MetadataService } from './metadata.service'; import { MetadataController } from './metadata.controller'; @Module({ - imports: [TypeOrmModule.forFeature([Template])], + imports: [TypeOrmModule.forFeature([TemplateEntity])], controllers: [TemplatesController, MetadataController], providers: [TemplatesService, MetadataService], exports: [TemplatesService, MetadataService], diff --git a/apps/issuer-frontend/Dockerfile b/apps/issuer-frontend/Dockerfile index 8bad8680..e5718618 100644 --- a/apps/issuer-frontend/Dockerfile +++ b/apps/issuer-frontend/Dockerfile @@ -1,5 +1,8 @@ FROM docker.io/nginx:stable-alpine +# Install wget +RUN apk --no-cache add wget + # Copy application files and the startup script with permissions COPY dist/apps/issuer-frontend/* /usr/share/nginx/html/ COPY --chmod=755 apps/issuer-frontend/startup.sh /usr/local/bin/startup.sh diff --git a/apps/issuer-frontend/project.json b/apps/issuer-frontend/project.json index d3d50ada..19211a4d 100644 --- a/apps/issuer-frontend/project.json +++ b/apps/issuer-frontend/project.json @@ -30,6 +30,7 @@ ], "styles": [ "@angular/material/prebuilt-themes/azure-blue.css", + "jsoneditor/dist/jsoneditor.min.css", "apps/issuer-frontend/src/styles.scss" ], "scripts": [], diff --git a/apps/issuer-frontend/src/app/app.component.html b/apps/issuer-frontend/src/app/app.component.html index 8c07cb09..fb4fef8e 100644 --- a/apps/issuer-frontend/src/app/app.component.html +++ b/apps/issuer-frontend/src/app/app.component.html @@ -1,5 +1,6 @@ Templates + Sessions
diff --git a/apps/issuer-frontend/src/app/app.routes.ts b/apps/issuer-frontend/src/app/app.routes.ts index 3fc3c416..68aaa3e5 100644 --- a/apps/issuer-frontend/src/app/app.routes.ts +++ b/apps/issuer-frontend/src/app/app.routes.ts @@ -1,10 +1,10 @@ import { Routes } from '@angular/router'; import { TemplatesListComponent } from './templates/templates-list/templates-list.component'; -import { TemplatesCreateComponent } from './templates/templates-create/templates-create.component'; import { TemplatesShowComponent } from './templates/templates-show/templates-show.component'; import { TemplatesIssueComponent } from './templates/templates-issue/templates-issue.component'; import { TemplatesEditComponent } from './templates/templates-edit/templates-edit.component'; import { SessionsShowComponent } from './sessions/sessions-show/sessions-show.component'; +import { SessionsListComponent } from './sessions/sessions-list/sessions-list.component'; export const routes: Routes = [ { @@ -21,7 +21,7 @@ export const routes: Routes = [ }, { path: 'new', - component: TemplatesCreateComponent, + component: TemplatesEditComponent, }, { path: ':id', @@ -35,8 +35,17 @@ export const routes: Routes = [ path: ':id/issue', component: TemplatesIssueComponent, }, + ], + }, + { + path: 'sessions', + children: [ + { + path: '', + component: SessionsListComponent, + }, { - path: ':id/sessions/:sessionId', + path: ':sessionId', component: SessionsShowComponent, }, ], diff --git a/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.html b/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.html index 250b627a..670050ab 100644 --- a/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.html +++ b/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.html @@ -30,7 +30,7 @@

Sessions

Correlation ID - {{ session.id }} + {{ session.id }} diff --git a/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.ts b/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.ts index a4858470..760c8232 100644 --- a/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.ts +++ b/apps/issuer-frontend/src/app/sessions/sessions-list/sessions-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CredentialOfferSession, @@ -31,6 +31,8 @@ import { SelectionModel } from '@angular/cdk/collections'; styleUrl: './sessions-list.component.scss', }) export class SessionsListComponent implements OnInit, OnDestroy { + @Input() id?: string; + interval!: ReturnType; dataSource = new MatTableDataSource(); displayedColumns = ['select', 'correlationId', 'status', 'timestamp']; @@ -42,13 +44,13 @@ export class SessionsListComponent implements OnInit, OnDestroy { } async ngOnInit(): Promise { - await this.loadSessions(); + this.loadSessions(); } private loadSessions() { - firstValueFrom(this.sessionsApiService.issuerControllerListAll()).then( - (sessions) => (this.dataSource.data = sessions) - ); + firstValueFrom( + this.sessionsApiService.issuerControllerListAll(this.id) + ).then((sessions) => (this.dataSource.data = sessions)); } /** Whether the number of selected elements matches the total number of rows. */ diff --git a/apps/issuer-frontend/src/app/sessions/sessions-show/sessions-show.component.html b/apps/issuer-frontend/src/app/sessions/sessions-show/sessions-show.component.html index 9e2cf7c5..0cdb4335 100644 --- a/apps/issuer-frontend/src/app/sessions/sessions-show/sessions-show.component.html +++ b/apps/issuer-frontend/src/app/sessions/sessions-show/sessions-show.component.html @@ -1,13 +1,5 @@
- - - - - - @@ -53,7 +45,12 @@

Credentials

}
- + + Template - -
diff --git a/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.scss b/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.scss deleted file mode 100644 index 89ff0c2b..00000000 --- a/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -#field { - width: 100vw; -} diff --git a/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.ts b/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.ts deleted file mode 100644 index d20430d4..00000000 --- a/apps/issuer-frontend/src/app/templates/templates-create/templates-create.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { - AbstractControl, - FormControl, - ReactiveFormsModule, - ValidationErrors, -} from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; -import { ActivatedRoute } from '@angular/router'; -import { Template, TemplatesApiService } from '@credhub/issuer-shared'; -import { firstValueFrom } from 'rxjs'; -import { Router } from '@angular/router'; -import { MatCardModule } from '@angular/material/card'; - -@Component({ - selector: 'app-templates-create', - standalone: true, - imports: [ - CommonModule, - ReactiveFormsModule, - MatInputModule, - MatButtonModule, - MatSnackBarModule, - MatCardModule, - ], - templateUrl: './templates-create.component.html', - styleUrl: './templates-create.component.scss', -}) -export class TemplatesCreateComponent implements OnInit { - control!: FormControl; - - constructor( - private templatesApiService: TemplatesApiService, - private router: Router, - private snackBar: MatSnackBar - ) {} - - async ngOnInit(): Promise { - this.control = new FormControl('{}', this.isValidJson); - } - - isValidJson(control: AbstractControl): ValidationErrors | null { - try { - JSON.parse(control.value); - } catch (e) { - return { invalidJson: true }; - } - return null; - } - - save(): void { - const content: Template = JSON.parse(this.control.value); - firstValueFrom( - this.templatesApiService.templatesControllerUpdate( - content.schema.id, - content - ) - ).then(() => - this.router - .navigate(['/templates']) - .then(() => - this.snackBar.open('Template created', 'Dismiss', { duration: 3000 }) - ) - ); - } -} diff --git a/apps/issuer-frontend/src/app/templates/templates-edit/schema.value.ts b/apps/issuer-frontend/src/app/templates/templates-edit/schema.value.ts new file mode 100644 index 00000000..b8d05005 --- /dev/null +++ b/apps/issuer-frontend/src/app/templates/templates-edit/schema.value.ts @@ -0,0 +1,146 @@ +export const schema = { + $schema: 'http://json-schema.org/draft-06/schema#', + $ref: '#/definitions/Template', + definitions: { + Template: { + type: 'object', + additionalProperties: false, + properties: { + schema: { + $ref: '#/definitions/Schema', + }, + sd: { + $ref: '#/definitions/SD', + }, + ttl: { + type: 'number', + }, + name: { + type: 'string', + }, + }, + required: ['schema', 'sd'], + title: 'Template', + }, + Schema: { + type: 'object', + additionalProperties: false, + properties: { + format: { + type: 'string', + }, + vct: { + type: 'string', + }, + claims: { + $ref: '#/definitions/Claims', + }, + display: { + type: 'array', + items: { + $ref: '#/definitions/SchemaDisplay', + }, + }, + }, + required: ['claims', 'display', 'format', 'vct'], + title: 'Schema', + }, + Claims: { + type: 'object', + additionalProperties: false, + patternProperties: { + '^[a-zA-Z]+$': { + type: 'object', + properties: { + display: { + type: 'array', + items: { + $ref: '#/definitions/ClaimDisplay', + }, + }, + }, + required: ['display'], + }, + }, + title: 'Claims', + }, + ClaimDisplay: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string', + }, + locale: { + type: 'string', + }, + }, + required: ['locale', 'name'], + title: 'ClaimDisplay', + }, + SchemaDisplay: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string', + }, + locale: { + type: 'string', + }, + logo: { + $ref: '#/definitions/BackgroundImage', + }, + background_image: { + $ref: '#/definitions/BackgroundImage', + }, + background_color: { + type: 'string', + }, + text_color: { + type: 'string', + }, + }, + required: [ + 'background_color', + 'background_image', + 'locale', + 'logo', + 'name', + 'text_color', + ], + title: 'SchemaDisplay', + }, + BackgroundImage: { + type: 'object', + additionalProperties: false, + properties: { + url: { + type: 'string', + format: 'uri', + 'qt-uri-protocols': ['https'], + 'qt-uri-extensions': ['.jpg', '.png'], + }, + alt_text: { + type: 'string', + }, + }, + required: ['alt_text', 'url'], + title: 'BackgroundImage', + }, + SD: { + type: 'object', + additionalProperties: false, + properties: { + _sd: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['_sd'], + title: 'SD', + }, + }, +}; diff --git a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.html b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.html index 7e9c120e..6aca6c1d 100644 --- a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.html +++ b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.html @@ -1,30 +1,21 @@ - - + + - {{ template.schema.id }} + {{ template.name }} - - Template - - Input is invalid - +
diff --git a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.scss b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.scss index 89ff0c2b..972344d4 100644 --- a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.scss +++ b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.scss @@ -1,3 +1,3 @@ -#field { - width: 100vw; +#editor { + height: 500px; } diff --git a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.ts b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.ts index ca42f24a..2cb530a8 100644 --- a/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.ts +++ b/apps/issuer-frontend/src/app/templates/templates-edit/templates-edit.component.ts @@ -1,14 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Template, TemplatesApiService } from '@credhub/issuer-shared'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { firstValueFrom } from 'rxjs'; -import { - AbstractControl, - FormControl, - ReactiveFormsModule, - ValidationErrors, -} from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; @@ -16,6 +11,8 @@ import { MatCardModule } from '@angular/material/card'; import { MatTabsModule } from '@angular/material/tabs'; import { MatIconModule } from '@angular/material/icon'; import { FlexLayoutModule } from 'ng-flex-layout'; +import { schema } from './schema.value'; +import JSONEditor, { JSONEditorOptions } from 'jsoneditor'; @Component({ selector: 'app-templates-edit', @@ -35,10 +32,13 @@ import { FlexLayoutModule } from 'ng-flex-layout'; templateUrl: './templates-edit.component.html', styleUrl: './templates-edit.component.scss', }) -export class TemplatesEditComponent implements OnInit { +export class TemplatesEditComponent implements AfterViewInit { + id?: string | null; template!: Template; - - control!: FormControl; + valid = true; + @ViewChild('jsonEditorContainer', { static: false }) + jsonEditorContainer!: ElementRef; + editor!: JSONEditor; constructor( private templatesApiService: TemplatesApiService, @@ -47,41 +47,57 @@ export class TemplatesEditComponent implements OnInit { private snackBar: MatSnackBar ) {} - async ngOnInit(): Promise { - const id = this.route.snapshot.paramMap.get('id'); - if (!id) { - return; + async ngAfterViewInit(): Promise { + this.id = this.route.snapshot.paramMap.get('id'); + if (this.id) { + this.template = await firstValueFrom( + this.templatesApiService.templatesControllerGetOne(this.id) + ).then((template) => template.value); + } else { + this.template = { + schema: { + format: 'vc+sd-jwt', + vct: '', + claims: {}, + display: [], + }, + sd: {}, + ttl: 0, + name: '', + }; } - this.template = await firstValueFrom( - this.templatesApiService.templatesControllerGetOne(id) - ); - this.control = new FormControl(JSON.stringify(this.template, null, 2), [ - this.isValidJson, - ]); + //TODO: instead the monaco-editor editor should be used, but it resulted in errors when building (no loader for ttf files). Also it was not clear how to make sure that the validation was working. But it had better auto completion and syntax highlighting. + const container = this.jsonEditorContainer.nativeElement; + const options: JSONEditorOptions = { + schema, + mode: 'code', + onChange: this.validate.bind(this), + }; + this.editor = new JSONEditor(container, options, this.template); + setTimeout(() => this.validate(), 100); } - isValidJson(control: AbstractControl): ValidationErrors | null { - try { - JSON.parse(control.value); - } catch (e) { - return { invalidJson: true }; + private async validate() { + const errors = await this.editor.validate(); + const hasSchemaErrors = errors && errors.length > 0; + if (hasSchemaErrors) { + this.valid = false; + return; } - return null; + this.valid = true; } save(): void { - const content: Template = JSON.parse(this.control.value); + const content: Template = this.editor.get(); firstValueFrom( this.templatesApiService.templatesControllerUpdate( - content.schema.id, + this.id as string, content ) ).then(() => this.router .navigate(['/templates']) - .then(() => - this.snackBar.open('Template saved', 'Dismiss', { duration: 3000 }) - ) + .then(() => this.snackBar.open('Template saved')) ); } } diff --git a/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.html b/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.html index de2e58fa..4a07394a 100644 --- a/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.html +++ b/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.html @@ -5,7 +5,7 @@ - {{ template.schema.id }} + {{ template.name }}
Status: {{ issuerService.statusEvent.value }}

- Go to session entry
diff --git a/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.ts b/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.ts index f18f9edd..b6c95698 100644 --- a/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.ts +++ b/apps/issuer-frontend/src/app/templates/templates-issue/templates-issue.component.ts @@ -5,6 +5,7 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { IssuerService, Template, + TemplateEntity, TemplatesApiService, } from '@credhub/issuer-shared'; import qrcode from 'qrcode'; @@ -43,7 +44,7 @@ export class TemplatesIssueComponent implements OnInit, OnDestroy { pinRequired = new FormControl(false); pinField = new FormControl(''); id!: string; - template!: Template; + template!: TemplateEntity; sessionId?: string; constructor( @@ -55,6 +56,7 @@ export class TemplatesIssueComponent implements OnInit, OnDestroy { this.form = new FormGroup({}); } async ngOnInit(): Promise { + //TODO: find out if it is expired, this should be realised via the status this.id = this.route.snapshot.paramMap.get('id') as string; this.template = await firstValueFrom( this.templatesApiService.templatesControllerGetOne(this.id) @@ -67,7 +69,7 @@ export class TemplatesIssueComponent implements OnInit, OnDestroy { } generateForm() { - for (const key in this.template.schema.claims) { + for (const key in this.template.value.schema.claims) { this.form.addControl(key, new FormControl('')); } } diff --git a/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.html b/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.html index 486df198..95826f0f 100644 --- a/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.html +++ b/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.html @@ -7,9 +7,9 @@ @for(template of templates; track template) { {{ template.schema.id }}{{ template.name }} } diff --git a/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.ts b/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.ts index 9b868350..656fa331 100644 --- a/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.ts +++ b/apps/issuer-frontend/src/app/templates/templates-list/templates-list.component.ts @@ -1,11 +1,15 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { Template, TemplatesApiService } from '@credhub/issuer-shared'; +import { + Template, + TemplateEntity, + TemplatesApiService, +} from '@credhub/issuer-shared'; import { firstValueFrom } from 'rxjs'; import { MatListModule } from '@angular/material/list'; import { RouterModule } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; -import { MatCard, MatCardModule } from '@angular/material/card'; +import { MatCardModule } from '@angular/material/card'; @Component({ selector: 'app-templates-list', @@ -21,7 +25,7 @@ import { MatCard, MatCardModule } from '@angular/material/card'; styleUrl: './templates-list.component.scss', }) export class TemplatesListComponent implements OnInit { - templates: Template[] = []; + templates: TemplateEntity[] = []; constructor(private templatesApiService: TemplatesApiService) {} async ngOnInit(): Promise { diff --git a/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.html b/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.html index ee1fc77f..348bebb7 100644 --- a/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.html +++ b/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.html @@ -5,7 +5,7 @@ - {{ template.schema.id }} + {{ template.name }} @@ -26,5 +26,5 @@ - +
diff --git a/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.ts b/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.ts index 2b49b84f..ac428df2 100644 --- a/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.ts +++ b/apps/issuer-frontend/src/app/templates/templates-show/templates-show.component.ts @@ -1,6 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { Template, TemplatesApiService } from '@credhub/issuer-shared'; +import { + Template, + TemplateEntity, + TemplatesApiService, +} from '@credhub/issuer-shared'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { FormControl } from '@angular/forms'; @@ -29,9 +33,10 @@ import { SessionsListComponent } from '../../sessions/sessions-list/sessions-lis styleUrl: './templates-show.component.scss', }) export class TemplatesShowComponent implements OnInit { - template!: Template; + template!: TemplateEntity; control!: FormControl; + id?: string | null; constructor( private templatesApiService: TemplatesApiService, @@ -41,12 +46,12 @@ export class TemplatesShowComponent implements OnInit { ) {} async ngOnInit(): Promise { - const id = this.route.snapshot.paramMap.get('id'); - if (!id) { + this.id = this.route.snapshot.paramMap.get('id'); + if (!this.id) { return; } this.template = await firstValueFrom( - this.templatesApiService.templatesControllerGetOne(id) + this.templatesApiService.templatesControllerGetOne(this.id) ); } @@ -55,9 +60,7 @@ export class TemplatesShowComponent implements OnInit { return; } firstValueFrom( - this.templatesApiService.templatesControllerDelete( - this.template.schema.id - ) + this.templatesApiService.templatesControllerDelete(this.template.id) ).then(() => this.router .navigate(['/templates']) diff --git a/apps/issuer-frontend/src/styles.scss b/apps/issuer-frontend/src/styles.scss index 5291ba94..29bff70c 100644 --- a/apps/issuer-frontend/src/styles.scss +++ b/apps/issuer-frontend/src/styles.scss @@ -2,3 +2,4 @@ // html, body { height: 100%; } body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } + diff --git a/apps/issuer-frontend/startup.sh b/apps/issuer-frontend/startup.sh index 2d29d9f5..4181c0c8 100644 --- a/apps/issuer-frontend/startup.sh +++ b/apps/issuer-frontend/startup.sh @@ -5,7 +5,6 @@ cat < /usr/share/nginx/html/assets/config.json { "backendUrl": "${BACKEND_URL}", "oidcUrl": "${OIDC_AUTH_URL}", - "credentialId": "${CREDENTIAL_ID}", "oidcClientId": "${OIDC_CLIENT_ID}", "oidcClientSecret": "${OIDC_CLIENT_SECRET}" } diff --git a/apps/verifier-backend/src/app/templates/dto/template.dto.ts b/apps/verifier-backend/src/app/templates/dto/template.dto.ts index 219ac9f5..0dab66c9 100644 --- a/apps/verifier-backend/src/app/templates/dto/template.dto.ts +++ b/apps/verifier-backend/src/app/templates/dto/template.dto.ts @@ -49,7 +49,10 @@ export class Request implements PresentationDefinitionV2 { @Type(() => InputDescriptor) input_descriptors: InputDescriptor[]; } -export class Template { +export class TemplateDto { + @IsString() + name: string; + @ValidateNested() @Type(() => Metadata) metadata: Metadata; diff --git a/apps/verifier-backend/src/app/templates/schemas/temoplate.entity.ts b/apps/verifier-backend/src/app/templates/schemas/temoplate.entity.ts index 15fc20e7..62d20b9c 100644 --- a/apps/verifier-backend/src/app/templates/schemas/temoplate.entity.ts +++ b/apps/verifier-backend/src/app/templates/schemas/temoplate.entity.ts @@ -1,9 +1,9 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; -import { Template as TemplateDTO } from '../dto/template.dto'; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { TemplateDto as TemplateDTO } from '../dto/template.dto'; @Entity() export class Template { - @PrimaryColumn() + @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'json' }) diff --git a/apps/verifier-backend/src/app/templates/templates.controller.ts b/apps/verifier-backend/src/app/templates/templates.controller.ts index 9a41f264..fa2e5090 100644 --- a/apps/verifier-backend/src/app/templates/templates.controller.ts +++ b/apps/verifier-backend/src/app/templates/templates.controller.ts @@ -11,7 +11,8 @@ import { import { ApiTags, ApiOAuth2, ApiOperation } from '@nestjs/swagger'; import { AuthGuard } from 'nest-keycloak-connect'; import { TemplatesService } from './templates.service'; -import { Template } from './dto/template.dto'; +import { TemplateDto } from './dto/template.dto'; +import { Template } from './schemas/temoplate.entity'; @ApiTags('templates') @UseGuards(AuthGuard) @@ -22,7 +23,7 @@ export class TemplatesController { @ApiOperation({ summary: 'List all templates' }) @Get() - listAll() { + listAll(): Promise { return this.templatesService.listAll(); } @@ -34,13 +35,13 @@ export class TemplatesController { @ApiOperation({ summary: 'Create a new template' }) @Post() - create(@Body() data: Template) { + create(@Body() data: TemplateDto) { return this.templatesService.create(data); } @ApiOperation({ summary: 'Update a template' }) @Patch(':id') - update(@Param('id') id: string, @Body() data: Template) { + update(@Param('id') id: string, @Body() data: TemplateDto) { return this.templatesService.update(id, data); } diff --git a/apps/verifier-backend/src/app/templates/templates.service.ts b/apps/verifier-backend/src/app/templates/templates.service.ts index 14a433eb..dbd7554a 100644 --- a/apps/verifier-backend/src/app/templates/templates.service.ts +++ b/apps/verifier-backend/src/app/templates/templates.service.ts @@ -1,20 +1,20 @@ import { ConflictException, Injectable } from '@nestjs/common'; -import { Template } from './dto/template.dto'; +import { TemplateDto } from './dto/template.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Template as TemplateEntity } from './schemas/temoplate.entity'; import { Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import { readdirSync, readFileSync } from 'fs'; import { join } from 'path'; import { validate } from 'class-validator'; import { plainToInstance } from 'class-transformer'; +import { Template } from './schemas/temoplate.entity'; @Injectable() export class TemplatesService { folder: string; constructor( - @InjectRepository(TemplateEntity) - private templateRepository: Repository, + @InjectRepository(Template) + private templateRepository: Repository