From 808a87745b64e2283eb23d518959f49d2accb544 Mon Sep 17 00:00:00 2001 From: vafeini <129304399+vafeini@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:07:53 +0300 Subject: [PATCH] Support mdl presentation (#69) * Introduced verification by mdl, refactored selectable attributes component to be agnostic of model * Moved presentation purpose out of model, it is specified per presentation case * Renamed component create-a-scenario --------- Co-authored-by: Vafeiadis Nikos --- package.json | 2 +- src/app/app-routing.module.ts | 24 +- src/app/core/data/age_attestation_pd.ts | 2 +- src/app/core/data/cbor_fields.ts | 285 ------------------ src/app/core/data/mdl_msoMdoc.ts | 42 +++ src/app/core/data/pid_age_over_18_pd.ts | 36 --- src/app/core/data/pid_msoMdoc.ts | 40 +++ .../core/data/pid_presentation_definition.ts | 203 ------------- src/app/core/models/CBORFields.ts | 7 - src/app/core/models/msoMdoc.ts | 11 + .../attestation-selectable-model.service.ts | 32 ++ .../services/mso-mdoc-presentation.service.ts | 59 ++++ ...online-authentication-siop.service.spec.ts | 14 +- .../online-authentication-siop.service.ts | 39 +-- .../home/components/home/home.component.html | 21 +- .../home/components/home/home.component.ts | 201 ++++++------ .../home/services/home.service.spec.ts | 1 + .../features/home/services/home.service.ts | 22 +- .../components/home/home.component.html | 18 +- .../components/home/home.component.spect.ts | 0 .../components/home/home.component.ts | 0 ...lectable-presentation-form.component.html} | 46 +-- ...lectable-presentation-form.component.scss} | 146 ++++----- ...table-presentation-form.component.spec.ts} | 10 +- ...selectable-presentation-form.component.ts} | 97 +++--- .../models/Constraint.ts | 5 + .../models/FieldConstraint.ts} | 2 +- .../models/FormSelectableField.ts | 7 + .../models/InputDescriptors.ts | 0 .../models/Presentation.ts | 0 .../models/PresentationDefinition.ts | 0 ...selectable-presentation-routing.module.ts} | 8 +- .../selectable-presentation.module.ts} | 8 +- .../helper-cbor-selectable.service.spec.ts | 0 .../helper-cbor-selectable.service.ts | 0 .../features/siop-custom/models/Constraint.ts | 5 - .../services/create-form.service.ts | 17 -- src/favicon.ico | Bin 87710 -> 15406 bytes 38 files changed, 559 insertions(+), 851 deletions(-) delete mode 100644 src/app/core/data/cbor_fields.ts create mode 100644 src/app/core/data/mdl_msoMdoc.ts delete mode 100644 src/app/core/data/pid_age_over_18_pd.ts create mode 100644 src/app/core/data/pid_msoMdoc.ts delete mode 100644 src/app/core/data/pid_presentation_definition.ts delete mode 100644 src/app/core/models/CBORFields.ts create mode 100644 src/app/core/models/msoMdoc.ts create mode 100644 src/app/core/services/attestation-selectable-model.service.ts create mode 100644 src/app/core/services/mso-mdoc-presentation.service.ts rename src/app/features/{siop-custom => selectable-presentation}/components/home/home.component.html (97%) rename src/app/features/{siop-custom => selectable-presentation}/components/home/home.component.spect.ts (100%) rename src/app/features/{siop-custom => selectable-presentation}/components/home/home.component.ts (100%) rename src/app/features/{siop-custom/components/create-a-scenario/create-a-scenario.component.html => selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html} (73%) rename src/app/features/{siop-custom/components/create-a-scenario/create-a-scenario.component.scss => selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss} (95%) rename src/app/features/{siop-custom/components/create-a-scenario/create-a-scenario.component.spec.ts => selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts} (77%) rename src/app/features/{siop-custom/components/create-a-scenario/create-a-scenario.component.ts => selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts} (53%) create mode 100644 src/app/features/selectable-presentation/models/Constraint.ts rename src/app/features/{siop-custom/models/DefinitionPath.ts => selectable-presentation/models/FieldConstraint.ts} (60%) create mode 100644 src/app/features/selectable-presentation/models/FormSelectableField.ts rename src/app/features/{siop-custom => selectable-presentation}/models/InputDescriptors.ts (100%) rename src/app/features/{siop-custom => selectable-presentation}/models/Presentation.ts (100%) rename src/app/features/{siop-custom => selectable-presentation}/models/PresentationDefinition.ts (100%) rename src/app/features/{siop-custom/cbor-selectable-routing.module.ts => selectable-presentation/selectable-presentation-routing.module.ts} (70%) rename src/app/features/{siop-custom/cbor-selectable.module.ts => selectable-presentation/selectable-presentation.module.ts} (72%) rename src/app/features/{siop-custom => selectable-presentation}/services/helper-cbor-selectable.service.spec.ts (100%) rename src/app/features/{siop-custom => selectable-presentation}/services/helper-cbor-selectable.service.ts (100%) delete mode 100644 src/app/features/siop-custom/models/Constraint.ts delete mode 100644 src/app/features/siop-custom/services/create-form.service.ts diff --git a/package.json b/package.json index 8860aa7..7e0ea4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verifier-ui", - "version": "0.3.0", + "version": "0.4.0-SNAPSHOT", "scripts": { "ng": "ng", "start": "npm run config && ng serve --proxy-config src/proxy.conf.json", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index dd8d473..c055649 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -9,19 +9,19 @@ import { NavigateService } from './core/services/navigate.service'; const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', loadComponent: () => import('./features/home/components/home/home.component').then(c => c.HomeComponent) }, - { path: 'presentation', + { path: 'custom-request', loadChildren: () => import('./features/presentation-definition/presentation-definition.module'). then(m => m.PresentationDefinitionModule )}, - { path: 'siop', - loadChildren: () => import('./features/siop/siop.module'). - then(m => m.SIOPModule )}, - { path: 'cbor', + // { path: 'siop', + // loadChildren: () => import('./features/siop/siop.module'). + // then(m => m.SIOPModule )}, + { path: 'pid-full', loadChildren: () => import('./features/cbor/cbor.module'). then(m => m.CborModule )}, { path: 'cbor-selectable', - loadChildren: () => import('./features/siop-custom/cbor-selectable.module'). - then(m => m.SiopCustomModule )}, - { path: 'age-over-18', + loadChildren: () => import('@features/selectable-presentation/selectable-presentation.module'). + then(m => m.SelectablePresentationModule )}, + { path: 'pid-age-over-18', loadChildren: () => import('./features/cbor/cbor.module'). then(m => m.CborModule ) }, @@ -29,6 +29,14 @@ const routes: Routes = [ loadChildren: () => import('./features/cbor/cbor.module'). then(m => m.CborModule ) }, + { path: 'mdl-selectable', + loadChildren: () => import('@features/selectable-presentation/selectable-presentation.module'). + then(m => m.SelectablePresentationModule ), + }, + { path: 'mdl-full', + loadChildren: () => import('./features/cbor/cbor.module'). + then(m => m.CborModule ) + }, { path: 'get-wallet-code', loadComponent: () => import('./features/wallet-redirect/wallet-redirect.component').then(c => c.WalletRedirectComponent), diff --git a/src/app/core/data/age_attestation_pd.ts b/src/app/core/data/age_attestation_pd.ts index 8da39e2..efb4fba 100644 --- a/src/app/core/data/age_attestation_pd.ts +++ b/src/app/core/data/age_attestation_pd.ts @@ -1,4 +1,4 @@ -import { Presentation } from '@app/features/siop-custom/models/Presentation'; +import { Presentation } from '@features/selectable-presentation/models/Presentation'; /* eslint-disable quotes */ export const AGE_ATTESTATION_OVER_18_PD: Presentation = { diff --git a/src/app/core/data/cbor_fields.ts b/src/app/core/data/cbor_fields.ts deleted file mode 100644 index e068e92..0000000 --- a/src/app/core/data/cbor_fields.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* eslint-disable quotes */ -import { CBORField } from '../models/CBORFields'; - -export const CBORFields: CBORField[] = [ - { - id: 1, - label: 'Family Name', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['family_name']" - ], - intent_to_retain: false - }, - }, - { - id: 2, - label: 'Given Name', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['given_name']" - ], - intent_to_retain: false - } - }, - { - id: 3, - label: 'Birthdate', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['birth_date']" - ], - intent_to_retain: false - } - }, - { - id: 4, - label: 'Age over 18', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['age_over_18']" - ], - intent_to_retain: false - } - }, - { - id: 5, - label: 'Age in years', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['age_in_years']" - ], - intent_to_retain: false - } - }, - { - id: 6, - label: 'Age birth years', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['age_birth_year']" - ], - intent_to_retain: false - } - }, - { - id: 7, - label: 'Family name birth', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['family_name_birth']" - ], - intent_to_retain: false - } - }, - { - id: 8, - label: 'Given name birth', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['given_name_birth']" - ], - intent_to_retain: false - } - }, - { - id: 9, - label: 'Birth place', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['birth_place']" - ], - intent_to_retain: false - } - }, - { - id: 10, - label: 'Birth country', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['birth_country']" - ], - intent_to_retain: false - } - }, - { - id: 11, - label: 'Birth state', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['birth_state']" - ], - intent_to_retain: false - } - }, - { - id: 12, - label: 'Birth city', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['birth_city']" - ], - intent_to_retain: false - } - }, - { - id: 13, - label: 'Resident address', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_address']" - ], - intent_to_retain: false - } - }, - { - id: 14, - label: 'Resident country', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_country']" - ], - intent_to_retain: false - } - }, - { - id: 15, - label: 'Resident state', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_state']" - ], - intent_to_retain: false - } - }, - { - id: 16, - label: 'Resident city', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_city']" - ], - intent_to_retain: false - } - }, - { - id: 17, - label: 'Resident postal code', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_postal_code']" - ], - intent_to_retain: false - } - }, - { - id: 18, - label: 'Resident street', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_street']" - ], - intent_to_retain: false - } - }, - { - id: 19, - label: 'Resident house number', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['resident_house_number']" - ], - intent_to_retain: false - } - }, - { - id: 20, - label: 'Gender', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['gender']" - ], - intent_to_retain: false - } - }, - { - id: 21, - label: 'Nationality', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['nationality']" - ], - intent_to_retain: false - } - }, - { - id: 22, - label: 'Issuance date', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['issuance_date']" - ], - intent_to_retain: false - } - }, - { - id: 23, - label: 'Expiry date', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['expiry_date']" - ], - intent_to_retain: false - } - }, - { - id: 24, - label: 'Issuing authority', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['issuing_authority']" - ], - intent_to_retain: false - } - }, - { - id: 25, - label: 'Document number', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['document_number']" - ], - intent_to_retain: false - } - }, - { - id: 26, - label: 'Administrative number', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['administrative_number']" - ], - intent_to_retain: false - } - }, - { - id: 27, - label: 'Issuing country', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['issuing_country']" - ], - intent_to_retain: false - } - }, - { - id: 28, - label: 'Issuing jurisdiction', - value: { - path: [ - "$['eu.europa.ec.eudi.pid.1']['issuing_jurisdiction']" - ], - intent_to_retain: false - } - }, -]; diff --git a/src/app/core/data/mdl_msoMdoc.ts b/src/app/core/data/mdl_msoMdoc.ts new file mode 100644 index 0000000..8467eb8 --- /dev/null +++ b/src/app/core/data/mdl_msoMdoc.ts @@ -0,0 +1,42 @@ +import {MsoMdoc} from "@core/models/msoMdoc"; + +/* eslint-disable quotes */ +export const MDL_MSO_MDOC: MsoMdoc = { + name: 'Mobile Driving Licence', + doctype: 'org.iso.18013.5.1.mDL', + namespace: 'org.iso.18013.5.1', + attributes: [ + { value: 'family_name', text: 'Family name' }, + { value: 'given_name', text: 'Given name'}, + { value: 'birth_date', text: 'Birthdate'}, + { value: 'issue_date', text: 'Issue date'}, + { value: 'expiry_date', text: 'Expiry date'}, + { value: 'age_over_18', text: 'Age over 18'}, + { value: 'age_over_21', text: 'Age over 21'}, + { value: 'age_in_years', text: 'Age in years'}, + { value: 'age_birth_year', text: 'Age birth year'}, + { value: 'issuing_authority', text: 'Issuing authority'}, + { value: 'document_number', text: 'Document number'}, + { value: 'portrait', text: 'Portrait'}, + { value: 'driving_privileges', text: 'Driving privileges'}, + { value: 'un_distinguishing_sign', text: 'Un-distinguishing sign'}, + { value: 'administrative_number', text: 'Administrative number'}, + { value: 'sex', text: 'Sex'}, + { value: 'height', text: 'Height'}, + { value: 'weight', text: 'Weight'}, + { value: 'eye_colour', text: 'Eye colour'}, + { value: 'hair_colour', text: 'Hair colour'}, + { value: 'birth_place', text: 'Birth place'}, + { value: 'resident_address', text: 'Resident address'}, + { value: 'portrait_capture_date', text: 'Portrait capture date'}, + { value: 'nationality', text: 'Nationality'}, + { value: 'resident_city', text: 'Resident city'}, + { value: 'resident_state', text: 'Resident state'}, + { value: 'resident_postal_code', text: 'Resident postal code'}, + { value: 'resident_country', text: 'Resident country'}, + { value: 'family_name_national_character', text: 'Family name national character'}, + { value: 'given_name_national_character', text: 'Given name national character'}, + { value: 'signature_usual_mark', text: 'Signature usual mark'}] +} + + diff --git a/src/app/core/data/pid_age_over_18_pd.ts b/src/app/core/data/pid_age_over_18_pd.ts deleted file mode 100644 index 012738e..0000000 --- a/src/app/core/data/pid_age_over_18_pd.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Presentation } from '@app/features/siop-custom/models/Presentation'; - -/* eslint-disable quotes */ -export const PID_AGE_OVER_18_PD: Presentation = { - 'type': 'vp_token', - 'presentation_definition': { - 'id': '32f54163-7166-48f1-93d8-ff217bdb0653', - 'input_descriptors': [ - { - 'id': 'eu.europa.ec.eudi.pid.1', - 'name': 'EUDI PID', - 'purpose': 'We need to verify you are over 18 using your PID', - 'format': { - 'mso_mdoc': { - 'alg': [ - "ES256", - "ES384", - "ES512" - ] - } - }, - 'constraints': { - 'fields': [ - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['age_over_18']" - ], - 'intent_to_retain': false - } - ] - } - } - ] - }, - 'nonce' : '' -}; diff --git a/src/app/core/data/pid_msoMdoc.ts b/src/app/core/data/pid_msoMdoc.ts new file mode 100644 index 0000000..c842d47 --- /dev/null +++ b/src/app/core/data/pid_msoMdoc.ts @@ -0,0 +1,40 @@ +import {MsoMdoc} from "@core/models/msoMdoc"; + +/* eslint-disable quotes */ +export const PID_MSO_MDOC: MsoMdoc = { + name: 'EUDI PID', + doctype: 'eu.europa.ec.eudi.pid.1', + namespace: 'eu.europa.ec.eudi.pid.1', + attributes: [ + { value: 'family_name', text: 'Family name'}, + { value: 'given_name', text: 'Given name'}, + { value: 'birth_date', text: 'Birthdate'}, + { value: 'age_over_18', text: 'Age over 18'}, + { value: 'age_in_years', text: 'Age in years'}, + { value: 'age_birth_year', text: 'Age birth year'}, + { value: 'family_name_birth', text: 'Family name birth'}, + { value: 'given_name_birth', text: 'Given name birth'}, + { value: 'birth_place', text: 'Birth place'}, + { value: 'birth_country', text: 'Birth country'}, + { value: 'birth_state', text: 'Birth state'}, + { value: 'birth_city', text: 'Birth city'}, + { value: 'resident_address', text: 'Resident address'}, + { value: 'resident_country', text: 'Resident country'}, + { value: 'resident_state', text: 'Resident state'}, + { value: 'resident_city', text: 'Resident city'}, + { value: 'resident_postal_code', text: 'Resident postal code'}, + { value: 'resident_street', text: 'Resident street'}, + { value: 'resident_house_number', text: 'Resident house number'}, + { value: 'gender', text: 'Gender'}, + { value: 'nationality', text: 'Nationality'}, + { value: 'issuance_date', text: 'Issuance date'}, + { value: 'expiry_date', text: 'Expiry date'}, + { value: 'issuing_authority', text: 'Issuing authority'}, + { value: 'document_number', text: 'Document number'}, + { value: 'administrative_number', text: 'Administrative number'}, + { value: 'issuing_country', text: 'Issuing country'}, + { value: 'issuing_jurisdiction', text: 'Issuing jurisdiction'} + ] +} + + diff --git a/src/app/core/data/pid_presentation_definition.ts b/src/app/core/data/pid_presentation_definition.ts deleted file mode 100644 index 4d828f7..0000000 --- a/src/app/core/data/pid_presentation_definition.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Presentation } from '@app/features/siop-custom/models/Presentation'; - -/* eslint-disable quotes */ -export const PID_PRESENTATION_DEFINITION: Presentation = { - 'type': 'vp_token', - 'presentation_definition': { - 'id': '32f54163-7166-48f1-93d8-ff217bdb0653', - 'input_descriptors': [ - { - 'id': 'eu.europa.ec.eudi.pid.1', - 'name': 'EUDI PID', - 'purpose': 'We need to verify your identity', - 'format': { - 'mso_mdoc': { - 'alg': [ - "ES256", - "ES384", - "ES512", - "EdDSA", - "ESB256", - "ESB320", - "ESB384", - "ESB512" - ] - } - }, - 'constraints': { - 'fields': [ - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['family_name']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['given_name']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['birth_date']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['age_over_18']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['age_in_years']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['age_birth_year']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['family_name_birth']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['given_name_birth']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['birth_place']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['birth_country']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['birth_state']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['birth_city']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_address']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_country']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_state']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_city']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_postal_code']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_street']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['resident_house_number']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['gender']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['nationality']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['issuance_date']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['expiry_date']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['issuing_authority']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['document_number']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['administrative_number']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['issuing_country']" - ], - 'intent_to_retain': false - }, - { - 'path': [ - "$['eu.europa.ec.eudi.pid.1']['issuing_jurisdiction']" - ], - 'intent_to_retain': false - } - ] - } - } - ] - }, - 'nonce' : '' -}; diff --git a/src/app/core/models/CBORFields.ts b/src/app/core/models/CBORFields.ts deleted file mode 100644 index 7db2be9..0000000 --- a/src/app/core/models/CBORFields.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DefinitionPath } from '@app/features/siop-custom/models/DefinitionPath'; - -export type CBORField = { - id: number, - label: string, - value: DefinitionPath -} diff --git a/src/app/core/models/msoMdoc.ts b/src/app/core/models/msoMdoc.ts new file mode 100644 index 0000000..005542d --- /dev/null +++ b/src/app/core/models/msoMdoc.ts @@ -0,0 +1,11 @@ +export type MsoMdoc = { + name: string; + doctype: string, + namespace: string, + attributes: Attribute[] +} + +export type Attribute = { + value: string, + text: string +} diff --git a/src/app/core/services/attestation-selectable-model.service.ts b/src/app/core/services/attestation-selectable-model.service.ts new file mode 100644 index 0000000..950ef9f --- /dev/null +++ b/src/app/core/services/attestation-selectable-model.service.ts @@ -0,0 +1,32 @@ +import {Injectable} from "@angular/core"; +import {Presentation} from "@features/selectable-presentation/models/Presentation"; +import {PID_MSO_MDOC} from '@core/data/pid_msoMdoc'; +import {MDL_MSO_MDOC} from '@core/data/mdl_msoMdoc'; +import {MsoMdoc} from "@core/models/msoMdoc"; + +@Injectable({ + providedIn: 'root' +}) +export class AttestationSelectableModelService { + + private selectableModel: MsoMdoc | null = null; + private presentationPurpose!: string; + + setPresentationPurpose(presentationPurpose: string) { + this.presentationPurpose = presentationPurpose; + } + + setModel(attestation: string) { + if (attestation == 'MDL') { + this.selectableModel = MDL_MSO_MDOC; + } else if (attestation == 'PID') { + this.selectableModel = PID_MSO_MDOC; + } + } + + getModel(): MsoMdoc { + return JSON.parse(JSON.stringify(this.selectableModel)); + } + + getPresentationPurpose(): string { return this.presentationPurpose} +} diff --git a/src/app/core/services/mso-mdoc-presentation.service.ts b/src/app/core/services/mso-mdoc-presentation.service.ts new file mode 100644 index 0000000..3c7b421 --- /dev/null +++ b/src/app/core/services/mso-mdoc-presentation.service.ts @@ -0,0 +1,59 @@ +import {Injectable} from "@angular/core"; +import {Attribute, MsoMdoc} from "@core/models/msoMdoc"; +import {Presentation} from "@features/selectable-presentation/models/Presentation"; +import {FieldConstraint} from "@features/selectable-presentation/models/FieldConstraint"; +import {uuidv4} from "@core/utils/uuid"; + +@Injectable({ + providedIn: 'root' +}) +export class MsoMdocPresentationService { + + fieldConstraint(document: MsoMdoc, attribute: string, intentToRetainOptional?: boolean): FieldConstraint { + var intentToRetain = false + if (typeof intentToRetainOptional !== 'undefined' && intentToRetainOptional) { + intentToRetain = true + } + return { + path: ['$[\''+document.namespace+'\'][\''+attribute+'\']'], + intent_to_retain: intentToRetain + } + } + + fieldConstraints(document: MsoMdoc, includeAttributes?: string[]): FieldConstraint[] { + var fieldConstraints: FieldConstraint[] = [] + document.attributes.forEach((attribute: Attribute) => { + if (typeof includeAttributes == 'undefined' || includeAttributes.includes(attribute.value)) { + fieldConstraints.push(this.fieldConstraint(document, attribute.value)); + } + }) + return fieldConstraints; + } + + presentationOf(document: MsoMdoc, presentationPurpose: string, includeAttributes?: string[]): Presentation { + return { + type: 'vp_token', + presentation_definition: { + id: uuidv4(), + input_descriptors: [{ + id: document.doctype, + name: document.name, + purpose: presentationPurpose, + format: { + 'mso_mdoc': { + 'alg': [ + "ES256", + "ES384", + "ES512" + ] + } + }, + constraints: { + fields: this.fieldConstraints(document, includeAttributes) + } + }] + }, + nonce: uuidv4() + }; + } +} diff --git a/src/app/core/services/online-authentication-siop.service.spec.ts b/src/app/core/services/online-authentication-siop.service.spec.ts index 76e77d9..a4c7d88 100644 --- a/src/app/core/services/online-authentication-siop.service.spec.ts +++ b/src/app/core/services/online-authentication-siop.service.spec.ts @@ -26,22 +26,10 @@ describe('OnlineAuthenticationSIOPService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); - it('#init transaction for the siop should return expected an object of PresentationDefinitionResponse (called once)', (done: DoneFn) => { - HttpServiceSpy.post.and.returnValue(asyncData(mocResponseData)); - - service.initTransaction().subscribe( - res => { - expect(res).toEqual(mocResponseData); - done(); - }, - done.fail - ); - expect(HttpServiceSpy.post.calls.count()).toBe(1, 'one call'); - }); it('#init transaction for the CBOR should return expected an object of PresentationDefinitionResponse (called once)', (done: DoneFn) => { HttpServiceSpy.post.and.returnValue(asyncData(mocResponseData)); - service.initCborTransaction().subscribe( + service.initPIDPresentationTransaction(presentationPurpose).subscribe( res => { expect(res).toEqual(mocResponseData); done(); diff --git a/src/app/core/services/online-authentication-siop.service.ts b/src/app/core/services/online-authentication-siop.service.ts index 21992c1..aca7e8e 100644 --- a/src/app/core/services/online-authentication-siop.service.ts +++ b/src/app/core/services/online-authentication-siop.service.ts @@ -3,13 +3,14 @@ import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { HttpService } from '../network/http/http.service'; import { PresentationDefinitionResponse } from '../models/presentation-definition-response'; -import { PID_PRESENTATION_DEFINITION } from '../data/pid_presentation_definition'; import { LocalStorageService } from './local-storage.service'; import * as constants from '@core/constants/constants'; import { DeviceDetectorService } from './device-detector.service'; import { uuidv4 } from '../utils/uuid'; -import { PID_AGE_OVER_18_PD } from '../data/pid_age_over_18_pd'; import { AGE_ATTESTATION_OVER_18_PD } from '../data/age_attestation_pd'; +import { MsoMdocPresentationService } from "@app/core/services/mso-mdoc-presentation.service"; +import { PID_MSO_MDOC } from '@core/data/pid_msoMdoc'; +import { MDL_MSO_MDOC } from "@core/data/mdl_msoMdoc"; @Injectable() export class OnlineAuthenticationSIOPService { @@ -17,26 +18,25 @@ export class OnlineAuthenticationSIOPService { constructor ( private readonly httpService: HttpService, private readonly localStorageService: LocalStorageService, - private readonly deviceDetectorService: DeviceDetectorService + private readonly deviceDetectorService: DeviceDetectorService, + private readonly msoMdocPresentationService: MsoMdocPresentationService ) { } - initTransaction (): Observable { - const dataRequest: any = { - 'type': 'id_token', - 'id_token_type': 'subject_signed_id_token', - 'nonce': uuidv4() - }; + initPIDPresentationTransaction(presentationPurpose: string): Observable { + let PID_FULL_PD = this.msoMdocPresentationService.presentationOf(PID_MSO_MDOC, presentationPurpose) + const payload: any = {...PID_FULL_PD}; + payload.nonce = uuidv4(); if (!this.deviceDetectorService.isDesktop()) { - dataRequest['wallet_response_redirect_uri_template'] = location.origin+'/get-wallet-code/?response_code={RESPONSE_CODE}'; + payload['wallet_response_redirect_uri_template'] = location.origin+'/get-wallet-code?response_code={RESPONSE_CODE}'; } - return this.httpService.post - ('ui/presentations', dataRequest) + return this.httpService.post('ui/presentations', payload) .pipe( tap((res) => { this.localStorageService.set(constants.UI_PRESENTATION, JSON.stringify(res));}) ); } - initCborTransaction (): Observable { - const payload: any = {...PID_PRESENTATION_DEFINITION}; + initPIDAgeOver18PresentationTransaction (presentationPurpose: string): Observable { + let PID_AGE_OVER_18_PD = this.msoMdocPresentationService.presentationOf(PID_MSO_MDOC, presentationPurpose, ["age_over_18"]) + const payload: any = {...PID_AGE_OVER_18_PD}; payload.nonce = uuidv4(); if (!this.deviceDetectorService.isDesktop()) { payload['wallet_response_redirect_uri_template'] = location.origin+'/get-wallet-code?response_code={RESPONSE_CODE}'; @@ -46,8 +46,9 @@ export class OnlineAuthenticationSIOPService { tap((res) => { this.localStorageService.set(constants.UI_PRESENTATION, JSON.stringify(res));}) ); } - initAgeOver18Transaction (): Observable { - const payload: any = {...PID_AGE_OVER_18_PD}; + + initAgeOver18AttestationPresentationTransaction (): Observable { + const payload: any = {...AGE_ATTESTATION_OVER_18_PD}; payload.nonce = uuidv4(); if (!this.deviceDetectorService.isDesktop()) { payload['wallet_response_redirect_uri_template'] = location.origin+'/get-wallet-code?response_code={RESPONSE_CODE}'; @@ -57,8 +58,10 @@ export class OnlineAuthenticationSIOPService { tap((res) => { this.localStorageService.set(constants.UI_PRESENTATION, JSON.stringify(res));}) ); } - initAgeOver18AttestationTransaction (): Observable { - const payload: any = {...AGE_ATTESTATION_OVER_18_PD}; + + initMDLPresentationTransaction(presentationPurpose: string): Observable { + let MDL_FULL_PD = this.msoMdocPresentationService.presentationOf(MDL_MSO_MDOC, presentationPurpose) + const payload: any = {...MDL_FULL_PD}; payload.nonce = uuidv4(); if (!this.deviceDetectorService.isDesktop()) { payload['wallet_response_redirect_uri_template'] = location.origin+'/get-wallet-code?response_code={RESPONSE_CODE}'; diff --git a/src/app/features/home/components/home/home.component.html b/src/app/features/home/components/home/home.component.html index 613d06e..7c9b28b 100644 --- a/src/app/features/home/components/home/home.component.html +++ b/src/app/features/home/components/home/home.component.html @@ -1,24 +1,31 @@
- - + + - + - - + + - + + + + + + + + - + diff --git a/src/app/features/home/components/home/home.component.ts b/src/app/features/home/components/home/home.component.ts index 9703f8c..d6637e8 100644 --- a/src/app/features/home/components/home/home.component.ts +++ b/src/app/features/home/components/home/home.component.ts @@ -1,101 +1,128 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { DataService } from '@app/core/services/data.service'; -import { NavigateService } from '@app/core/services/navigate.service'; -import { OnlineAuthenticationSIOPService } from '@app/core/services/online-authentication-siop.service'; -import { RadioGroupComponent } from '@app/shared/elements/radio-group/radio-group.component'; -import { SharedModule } from '@app/shared/shared.module'; -import { HomeService } from '../../services/home.service'; -import { MenuOption } from '../../models/menu-option'; -import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; -import { BodyAction } from '@app/shared/elements/body-actions/models/BodyAction'; -import { HOME_ACTIONS } from '@app/core/utils/pages-actions'; -import { LocalStorageService } from '@app/core/services/local-storage.service'; +import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {DataService} from '@app/core/services/data.service'; +import {NavigateService} from '@app/core/services/navigate.service'; +import {OnlineAuthenticationSIOPService} from '@app/core/services/online-authentication-siop.service'; +import {RadioGroupComponent} from '@app/shared/elements/radio-group/radio-group.component'; +import {SharedModule} from '@app/shared/shared.module'; +import {HomeService} from '../../services/home.service'; +import {MenuOption} from '../../models/menu-option'; +import {WalletLayoutComponent} from '@app/core/layout/wallet-layout/wallet-layout.component'; +import {BodyAction} from '@app/shared/elements/body-actions/models/BodyAction'; +import {HOME_ACTIONS} from '@app/core/utils/pages-actions'; +import {LocalStorageService} from '@app/core/services/local-storage.service'; import * as constants from '@core/constants/constants'; -import { InputSchemeComponent } from '../input-scheme/input-scheme.component'; -import { MatTabsModule } from '@angular/material/tabs'; +import {InputSchemeComponent} from '../input-scheme/input-scheme.component'; +import {MatTabsModule} from '@angular/material/tabs'; +import {AttestationSelectableModelService} from "@app/core/services/attestation-selectable-model.service"; @Component({ - standalone: true, - imports: [CommonModule, MatTabsModule, RadioGroupComponent, SharedModule, InputSchemeComponent, WalletLayoutComponent], - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'], - providers: [OnlineAuthenticationSIOPService, HomeService], - changeDetection: ChangeDetectionStrategy.OnPush + standalone: true, + imports: [CommonModule, MatTabsModule, RadioGroupComponent, SharedModule, InputSchemeComponent, WalletLayoutComponent], + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], + providers: [OnlineAuthenticationSIOPService, HomeService], + changeDetection: ChangeDetectionStrategy.OnPush }) export class HomeComponent implements OnInit { - actions: BodyAction[] = HOME_ACTIONS; - optionsCustomRequest: MenuOption[] = []; - optionsPIDAuthentication: MenuOption[] = []; - optionsAgeVerification: MenuOption[] = []; - constructor ( + actions: BodyAction[] = HOME_ACTIONS; + optionsCustomRequest: MenuOption[] = []; + optionsPIDAuthentication: MenuOption[] = []; + optionsMDLAuthentication: MenuOption[] = []; + optionsAgeVerification: MenuOption[] = []; + + constructor( private navigateService: NavigateService, private readonly onlineAuthenticationSIOPService: OnlineAuthenticationSIOPService, private readonly dataService: DataService, + private readonly attestationSelectableModelService: AttestationSelectableModelService, private readonly homeService: HomeService, private readonly localStorageService: LocalStorageService - ) { - this.localStorageService.remove(constants.UI_PRESENTATION); - } - ngOnInit (): void { - this.optionsCustomRequest = this.homeService.optionsCustomRequest; - this.optionsPIDAuthentication = this.homeService.optionsPIDAuthentication; - this.optionsAgeVerification = this.homeService.optionsAgeVerification; - } + ) { + this.localStorageService.remove(constants.UI_PRESENTATION); + } + + ngOnInit(): void { + this.optionsCustomRequest = this.homeService.optionsCustomRequest; + this.optionsPIDAuthentication = this.homeService.optionsPIDAuthentication; + this.optionsMDLAuthentication = this.homeService.optionsMDLAuthentication; + this.optionsAgeVerification = this.homeService.optionsAgeVerification; + } + + private navTarget = ''; + + setNavigateTarget(choose: string) { + if (choose === 'PID_full') { + this.navTarget = 'pid-full'; + } else if (choose === 'PID_Selectable') { + this.navTarget = 'cbor-selectable/pid-create'; + } else if (choose === 'AgeOver18_attestation') { + this.navTarget = 'age-attestation'; + } else if (choose === 'AgeOver18_pid') { + this.navTarget = 'pid-age-over-18'; + } else if (choose === 'MDL_Selectable') { + this.navTarget = 'cbor-selectable/mdl-create'; + } else if (choose === 'MDL_Full') { + this.navTarget = 'mdl-full'; + } else if (choose === 'PD_Custom_Request') { + this.navTarget = 'custom-request'; + } + this.actions = [...this.actions].map((item) => { + item.disabled = false; + return item; + }); + } + + submit() { + if (this.navTarget === 'pid-full') { + let presentationPurpose = 'We need to verify your identity'; + this.onlineAuthenticationSIOPService.initPIDPresentationTransaction(presentationPurpose).subscribe((data) => { + this.dataService.setQRCode(data); + this.navigateService.navigateTo(this.navTarget); + }); + + } else if (this.navTarget === 'cbor-selectable/pid-create') { + let presentationPurpose = 'We need to verify your identity'; + this.attestationSelectableModelService.setPresentationPurpose(presentationPurpose) + this.attestationSelectableModelService.setModel('PID') + this.navigateService.navigateTo('cbor-selectable/create'); + + } else if (this.navTarget === 'mdl-full') { + let presentationPurpose = 'We need to verify your mobile driving licence'; + this.onlineAuthenticationSIOPService.initMDLPresentationTransaction(presentationPurpose).subscribe((data) => { + this.dataService.setQRCode(data); + this.navigateService.navigateTo(this.navTarget); + }); + + } else if (this.navTarget === 'cbor-selectable/mdl-create') { + let presentationPurpose = 'We need to verify your mobile driving licence'; + this.attestationSelectableModelService.setPresentationPurpose(presentationPurpose) + this.attestationSelectableModelService.setModel('MDL') + this.navigateService.navigateTo('cbor-selectable/create'); + + } else if (this.navTarget === 'pid-age-over-18') { + let presentationPurpose = 'We need to verify you are over 18 using your PID'; + this.onlineAuthenticationSIOPService.initPIDAgeOver18PresentationTransaction(presentationPurpose).subscribe((data) => { + this.dataService.setQRCode(data); + this.navigateService.navigateTo(this.navTarget); + }); + + } else if (this.navTarget === 'age-attestation') { + this.onlineAuthenticationSIOPService.initAgeOver18AttestationPresentationTransaction().subscribe((data) => { + this.dataService.setQRCode(data); + this.navigateService.navigateTo(this.navTarget); + }); - private navPath = ''; + } else if (this.navTarget === 'custom-request') { + this.navigateService.navigateTo(this.navTarget); + } + } - navigate (choose: string) { - if (choose === 'SIOP') { - this.navPath = 'siop'; - } else if (choose === 'OID4VP_CBOR') { - this.navPath = 'cbor'; - } else if (choose === 'OID4VP_C') { - this.navPath = '/presentation'; - } else if (choose === 'OID4VP_CBOR_Selectable') { - this.navPath = 'cbor-selectable/create'; - } else if (choose === 'OID4VP_attestation') { - this.navPath = 'age-attestation'; - } else if (choose === 'OID4VP_age_over_18') { - this.navPath = 'age-over-18'; - } - this.actions = [...this.actions].map((item) => { - item.disabled = false; - return item; - }); - } - submit () { - if (this.navPath === '/presentation') { - this.navigateService.navigateTo(this.navPath); - } else if (this.navPath === 'siop') { - this.onlineAuthenticationSIOPService.initTransaction().subscribe((data) => { - this.dataService.setQRCode(data); - this.navigateService.navigateTo(this.navPath); - }); - } else if(this.navPath === 'cbor') { - this.onlineAuthenticationSIOPService.initCborTransaction().subscribe((data) => { - this.dataService.setQRCode(data); - this.navigateService.navigateTo(this.navPath); - }); - } else if (this.navPath === 'cbor-selectable/create') { - this.navigateService.navigateTo(this.navPath); - } else if (this.navPath === 'age-over-18') { - this.onlineAuthenticationSIOPService.initAgeOver18Transaction().subscribe((data) => { - this.dataService.setQRCode(data); - this.navigateService.navigateTo(this.navPath); - }); - } else if (this.navPath === 'age-attestation') { - this.onlineAuthenticationSIOPService.initAgeOver18AttestationTransaction().subscribe((data) => { - this.dataService.setQRCode(data); - this.navigateService.navigateTo(this.navPath); - }); - } - } - selectedIndexChange (_event: number) { - this.actions = [...this.actions].map((item) => { - item.disabled = true; - return item; - }); - } + selectedIndexChange(_event: number) { + this.actions = [...this.actions].map((item) => { + item.disabled = true; + return item; + }); + } } diff --git a/src/app/features/home/services/home.service.spec.ts b/src/app/features/home/services/home.service.spec.ts index 8560683..e70a618 100644 --- a/src/app/features/home/services/home.service.spec.ts +++ b/src/app/features/home/services/home.service.spec.ts @@ -13,6 +13,7 @@ describe('HomeService', () => { }); it('should be created', () => { + // @ts-ignore expect(service).toBeTruthy(); }); }); diff --git a/src/app/features/home/services/home.service.ts b/src/app/features/home/services/home.service.ts index d3e7c46..5644df5 100644 --- a/src/app/features/home/services/home.service.ts +++ b/src/app/features/home/services/home.service.ts @@ -6,31 +6,43 @@ export class HomeService { optionsPIDAuthentication: MenuOption[] = [ { - key: 'OID4VP_CBOR_Selectable', + key: 'PID_Selectable', value: 'Request to share specific attributes from PID', isDisabled: false, }, { - key: 'OID4VP_CBOR', + key: 'PID_full', value: 'Request for the entire PID', isDisabled: false, } ]; + optionsMDLAuthentication: MenuOption[] = [ + { + key: 'MDL_Selectable', + value: 'Request to share specific attributes from mDL', + isDisabled: false, + }, + { + key: 'MDL_Full', + value: 'Request for the entire mDL', + isDisabled: false, + } + ]; optionsAgeVerification: MenuOption[] = [ { - key: 'OID4VP_attestation', + key: 'AgeOver18_attestation', value: 'Age over 18 (age attestation)', isDisabled: false, }, { - key: 'OID4VP_age_over_18', + key: 'AgeOver18_pid', value: 'Age over 18 (PID)', isDisabled: false, } ]; optionsCustomRequest: MenuOption[] = [ { - key: 'OID4VP_C', + key: 'PD_Custom_Request', value: 'Custom request (for testing)', isDisabled: false, } diff --git a/src/app/features/siop-custom/components/home/home.component.html b/src/app/features/selectable-presentation/components/home/home.component.html similarity index 97% rename from src/app/features/siop-custom/components/home/home.component.html rename to src/app/features/selectable-presentation/components/home/home.component.html index ede9842..ed3710b 100644 --- a/src/app/features/siop-custom/components/home/home.component.html +++ b/src/app/features/selectable-presentation/components/home/home.component.html @@ -1,9 +1,9 @@ - -
-
Proceed to authentication
-
- -
- -
-
+ +
+
Proceed to authentication
+
+ +
+ +
+
diff --git a/src/app/features/siop-custom/components/home/home.component.spect.ts b/src/app/features/selectable-presentation/components/home/home.component.spect.ts similarity index 100% rename from src/app/features/siop-custom/components/home/home.component.spect.ts rename to src/app/features/selectable-presentation/components/home/home.component.spect.ts diff --git a/src/app/features/siop-custom/components/home/home.component.ts b/src/app/features/selectable-presentation/components/home/home.component.ts similarity index 100% rename from src/app/features/siop-custom/components/home/home.component.ts rename to src/app/features/selectable-presentation/components/home/home.component.ts diff --git a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.html b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html similarity index 73% rename from src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.html rename to src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html index bb57443..1e45187 100644 --- a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.html +++ b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.html @@ -1,23 +1,23 @@ -
-
-
-

Select the information requested from the user

- {{definitionFields.length}} selected -
- -
- - {{field.label}} - -
-
-
-
- - - - Presentation definition - - - - +
+
+
+

Select the information requested from the user

+ {{selectedFields.length}} selected +
+ +
+ + {{field.label}} + +
+
+
+
+ + + + Presentation definition + + + + diff --git a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.scss b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss similarity index 95% rename from src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.scss rename to src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss index 7738f9c..1630943 100644 --- a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.scss +++ b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.scss @@ -1,73 +1,73 @@ -@use '/src/template' as temp; -@use '/src//layout-breakpoint' as points; - -:host { - max-width: 40rem; - - ::ng-deep { - .mat-expansion-panel { - padding: 0 !important; - } - - } - - .mb { - margin-bottom: temp.$spaceBasic; - } - - .creation { - display: flex; - flex-direction: column; - height: temp.$body-height; - overflow-y: auto; - - .creation-header { - align-items: baseline; - background: temp.$backgroundDefault; - display: flex; - flex-direction: row; - justify-content: space-between; - position: sticky; - top: 0; - z-index: 50; - - h3 { - color: temp.$textDarkSecondary; - font-size: 16px; - -webkit-font-smoothing: antialiased; - letter-spacing: 0; - line-height: 20px; - overflow-wrap: break-word; - text-decoration: none; - word-break: break-word; - } - - span { - color: temp.$textDarkSecondary; - padding-right: temp.$spaceBasic; - } - } - - .controls { - display: flex; - flex-direction: row; - } - - .presentation-definition { - margin-top: 1rem; - } - } - @media (max-width: map-get(points.$breakpoints, xs)) and (min-width: map-get(points.$breakpoints, sm)){ - .creation { - height: 24.25rem; - width: temp.$body-width-xs; - } - } - @media (max-width: map-get(points.$breakpoints, sm)) { - .creation { - height: 24.25rem; - width: temp.$body-width-sm; - } - } - -} +@use '/src/template' as temp; +@use '/src//layout-breakpoint' as points; + +:host { + max-width: 40rem; + + ::ng-deep { + .mat-expansion-panel { + padding: 0 !important; + } + + } + + .mb { + margin-bottom: temp.$spaceBasic; + } + + .creation { + display: flex; + flex-direction: column; + height: temp.$body-height; + overflow-y: auto; + + .creation-header { + align-items: baseline; + background: temp.$backgroundDefault; + display: flex; + flex-direction: row; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 50; + + h3 { + color: temp.$textDarkSecondary; + font-size: 16px; + -webkit-font-smoothing: antialiased; + letter-spacing: 0; + line-height: 20px; + overflow-wrap: break-word; + text-decoration: none; + word-break: break-word; + } + + span { + color: temp.$textDarkSecondary; + padding-right: temp.$spaceBasic; + } + } + + .controls { + display: flex; + flex-direction: row; + } + + .presentation-definition { + margin-top: 1rem; + } + } + @media (max-width: map-get(points.$breakpoints, xs)) and (min-width: map-get(points.$breakpoints, sm)){ + .creation { + height: 24.25rem; + width: temp.$body-width-xs; + } + } + @media (max-width: map-get(points.$breakpoints, sm)) { + .creation { + height: 24.25rem; + width: temp.$body-width-sm; + } + } + +} diff --git a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.spec.ts b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts similarity index 77% rename from src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.spec.ts rename to src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts index 6f03f2f..b01e058 100644 --- a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.spec.ts +++ b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; import { RouterModule } from '@angular/router'; import { SharedModule } from '@app/shared/shared.module'; -import { CreateAScenarioComponent } from './create-a-scenario.component'; +import { SelectablePresentationFormComponent } from './selectable-presentation-form.component'; import { HttpClientModule } from '@angular/common/http'; import { HelperCborSelectableService } from '../../services/helper-cbor-selectable.service'; import { MatExpansionModule } from '@angular/material/expansion'; @@ -11,8 +11,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatCheckboxModule } from '@angular/material/checkbox'; describe('CBOR CreateAScenarioComponent', () => { - let component: CreateAScenarioComponent; - let fixture: ComponentFixture; + let component: SelectablePresentationFormComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -26,14 +26,14 @@ describe('CBOR CreateAScenarioComponent', () => { SharedModule, MatCheckboxModule, ], - declarations: [ CreateAScenarioComponent ], + declarations: [ SelectablePresentationFormComponent ], providers: [ HelperCborSelectableService ] }) .compileComponents(); - fixture = TestBed.createComponent(CreateAScenarioComponent); + fixture = TestBed.createComponent(SelectablePresentationFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.ts b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts similarity index 53% rename from src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.ts rename to src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts index b955664..2450551 100644 --- a/src/app/features/siop-custom/components/create-a-scenario/create-a-scenario.component.ts +++ b/src/app/features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component.ts @@ -1,72 +1,77 @@ import { Component, OnInit, ChangeDetectorRef, Injector } from '@angular/core'; -import { FormGroup } from '@angular/forms'; import { catchError } from 'rxjs'; import { PresentationDefinitionResponse } from '@core/models/presentation-definition-response'; import { PresentationDefinitionService } from '@app/core/services/presentation-definition.service'; -import { CreateFormService } from '../../services/create-form.service'; -import { PID_PRESENTATION_DEFINITION } from '@app/core/data/pid_presentation_definition'; -import { DefinitionPath } from '../../models/DefinitionPath'; +import { FieldConstraint } from '../../models/FieldConstraint'; import { DataService } from '@app/core/services/data.service'; import { NavigateService } from '@app/core/services/navigate.service'; -import { CBORFields } from '@app/core/data/cbor_fields'; -import { CBORField } from '@app/core/models/CBORFields'; +import { FormSelectableField } from '@features/selectable-presentation/models/FormSelectableField'; import { HelperCborSelectableService } from '../../services/helper-cbor-selectable.service'; import { LocalStorageService } from '@app/core/services/local-storage.service'; import * as constants from '@core/constants/constants'; -import { uuidv4 } from '@app/core/utils/uuid'; import { Modification } from '@app/shared/elements/body-actions/models/modification'; import { BodyActionsService } from '@app/shared/elements/body-actions/body-actions.service'; import { Presentation } from '../../models/Presentation'; +import { AttestationSelectableModelService } from "@app/core/services/attestation-selectable-model.service"; +import { MsoMdocPresentationService } from "@app/core/services/mso-mdoc-presentation.service"; +import { MsoMdoc } from "@core/models/msoMdoc"; + @Component({ selector: 'vc-create-a-scenario', - templateUrl: './create-a-scenario.component.html', - styleUrls: ['./create-a-scenario.component.scss'], - providers: [CreateFormService, PresentationDefinitionService] + templateUrl: './selectable-presentation-form.component.html', + styleUrls: ['./selectable-presentation-form.component.scss'], + providers: [PresentationDefinitionService] }) -export class CreateAScenarioComponent implements OnInit { +export class SelectablePresentationFormComponent implements OnInit { - form!: FormGroup; - fields: CBORField[]; + formFields!: FormSelectableField[]; requestGenerate = false; buttonMode = 'none'; - readonly initDefinitionObject = JSON.parse(JSON.stringify(PID_PRESENTATION_DEFINITION)); - definition!: Presentation; - definitionText!: string; - definitionFields: DefinitionPath[] = []; + attestationModel!: MsoMdoc; + draftPresentation!: Presentation; + presentationDefinitionText!: string; + selectedFields: FieldConstraint[] = []; private readonly navigateService!: NavigateService; private readonly helperCborSelectableService!: HelperCborSelectableService; private readonly localStorageService!: LocalStorageService; private readonly bodyActionsService!: BodyActionsService; constructor ( - private readonly createFormService: CreateFormService, private readonly presentationDefinitionService: PresentationDefinitionService, + private readonly attestationSelectableModelService: AttestationSelectableModelService, + private readonly msoMdocPresentationService: MsoMdocPresentationService, private readonly dataService: DataService, private readonly changeDetectorRef: ChangeDetectorRef, private readonly injector: Injector, ) { - this.definition = this.initDefinitionObject; - this.definition.nonce = uuidv4(); this.navigateService = this.injector.get(NavigateService); this.helperCborSelectableService = this.injector.get(HelperCborSelectableService); this.localStorageService = this.injector.get(LocalStorageService); this.bodyActionsService = this.injector.get(BodyActionsService); - this.form = this.createFormService.form; - this.fields = CBORFields; + + this.enableNextButton(); } + ngOnInit (): void { this.localStorageService.remove(constants.UI_PRESENTATION); - this.setNextButton(); - this.setFields(); - this.definitionText = this.convertJSONtoString(this.definition.presentation_definition); + this.initPresentationModel(); + // Init form from model + this.formFields = this.extractFormFieldsFromModel() this.helperCborSelectableService.goNextStep$.subscribe(_ => { this.generateCode(); }); } + + initPresentationModel() { + this.attestationModel = this.attestationSelectableModelService.getModel(); + var presentationPurpose = this.attestationSelectableModelService.getPresentationPurpose(); + this.draftPresentation = this.msoMdocPresentationService.presentationOf(this.attestationModel, presentationPurpose, []) + } + generateCode () { this.requestGenerate = true; - if (this.convertJSONtoString(this.definition)) { + if (this.convertJSONtoString(this.draftPresentation)) { this.buttonMode = 'loading'; - this.presentationDefinitionService.generateCode(this.convertJSONtoString(this.definition)) + this.presentationDefinitionService.generateCode(this.convertJSONtoString(this.draftPresentation)) .pipe( catchError((error) => { return error; @@ -83,40 +88,50 @@ export class CreateAScenarioComponent implements OnInit { console.log('invalid JSON'); } } - handle (data: CBORField) { + handle (data: FormSelectableField) { const value = data?.value; if (!this.isExist(value.path[0])) { - this.definitionFields.push(value); + this.selectedFields.push(value); } else if (this.isExist(value.path[0])) { - this.definitionFields = this.definitionFields.filter((item: DefinitionPath) => { + this.selectedFields = this.selectedFields.filter((item: FieldConstraint) => { return String(item.path) !== String(value.path[0]); }); } - this.setFields(); - this.definitionText = this.convertJSONtoString(this.definition.presentation_definition); - this.setNextButton(); + // Update draft presentation with selected fields + this.draftPresentation.presentation_definition.input_descriptors[0].constraints.fields = this.selectedFields; + // refresh PD text from model + this.presentationDefinitionText = this.convertJSONtoString(this.draftPresentation.presentation_definition); + this.enableNextButton(); this.changeDetectorRef.detectChanges(); } - setFields () { - this.definition.presentation_definition.input_descriptors[0].constraints.fields = this.definitionFields; - } - convertJSONtoString (obj: object) { + convertJSONtoString (obj: object) { return JSON.stringify(obj, null, '\t'); } + isExist (path: string) { - const exists = this.definitionFields.filter((item) => item.path.includes(path)); + const exists = this.selectedFields.filter((item) => item.path.includes(path)); return exists.length > 0; } - setNextButton () { + + enableNextButton () { const modifyData: Modification = { id: 'next_button', - disabled: this.definitionFields.length === 0 + disabled: this.selectedFields == undefined || this.selectedFields.length === 0 }; this.bodyActionsService.handelButton$.next(modifyData); } - trackByFn (_index: number, data: CBORField) { + extractFormFieldsFromModel(): FormSelectableField[] { + return this.attestationModel.attributes.map( (attr, index) => { + return { + id: index, + label: attr.text, + value: this.msoMdocPresentationService.fieldConstraint(this.attestationModel, attr.value) + } + }) + } + trackByFn (_index: number, data: FormSelectableField) { return data.id; } } diff --git a/src/app/features/selectable-presentation/models/Constraint.ts b/src/app/features/selectable-presentation/models/Constraint.ts new file mode 100644 index 0000000..7c7aee0 --- /dev/null +++ b/src/app/features/selectable-presentation/models/Constraint.ts @@ -0,0 +1,5 @@ +import { FieldConstraint } from './FieldConstraint'; + +export type Constraint = { + fields: FieldConstraint[] +} diff --git a/src/app/features/siop-custom/models/DefinitionPath.ts b/src/app/features/selectable-presentation/models/FieldConstraint.ts similarity index 60% rename from src/app/features/siop-custom/models/DefinitionPath.ts rename to src/app/features/selectable-presentation/models/FieldConstraint.ts index 2614b56..19c82a6 100644 --- a/src/app/features/siop-custom/models/DefinitionPath.ts +++ b/src/app/features/selectable-presentation/models/FieldConstraint.ts @@ -1,4 +1,4 @@ -export type DefinitionPath = { +export type FieldConstraint = { path: string[], intent_to_retain: boolean, } diff --git a/src/app/features/selectable-presentation/models/FormSelectableField.ts b/src/app/features/selectable-presentation/models/FormSelectableField.ts new file mode 100644 index 0000000..5f905af --- /dev/null +++ b/src/app/features/selectable-presentation/models/FormSelectableField.ts @@ -0,0 +1,7 @@ +import { FieldConstraint } from '@features/selectable-presentation/models/FieldConstraint'; + +export type FormSelectableField = { + id: number, + label: string, + value: FieldConstraint +} diff --git a/src/app/features/siop-custom/models/InputDescriptors.ts b/src/app/features/selectable-presentation/models/InputDescriptors.ts similarity index 100% rename from src/app/features/siop-custom/models/InputDescriptors.ts rename to src/app/features/selectable-presentation/models/InputDescriptors.ts diff --git a/src/app/features/siop-custom/models/Presentation.ts b/src/app/features/selectable-presentation/models/Presentation.ts similarity index 100% rename from src/app/features/siop-custom/models/Presentation.ts rename to src/app/features/selectable-presentation/models/Presentation.ts diff --git a/src/app/features/siop-custom/models/PresentationDefinition.ts b/src/app/features/selectable-presentation/models/PresentationDefinition.ts similarity index 100% rename from src/app/features/siop-custom/models/PresentationDefinition.ts rename to src/app/features/selectable-presentation/models/PresentationDefinition.ts diff --git a/src/app/features/siop-custom/cbor-selectable-routing.module.ts b/src/app/features/selectable-presentation/selectable-presentation-routing.module.ts similarity index 70% rename from src/app/features/siop-custom/cbor-selectable-routing.module.ts rename to src/app/features/selectable-presentation/selectable-presentation-routing.module.ts index 09fe5f1..2f497a9 100644 --- a/src/app/features/siop-custom/cbor-selectable-routing.module.ts +++ b/src/app/features/selectable-presentation/selectable-presentation-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './components/home/home.component'; -import { CreateAScenarioComponent } from './components/create-a-scenario/create-a-scenario.component'; +import { SelectablePresentationFormComponent } from '@features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component'; const routes: Routes = [ { path: '', @@ -13,7 +13,11 @@ const routes: Routes = [ }, { path: 'create', - component: CreateAScenarioComponent + component: SelectablePresentationFormComponent + }, + { + path: 'pid-create', + component: SelectablePresentationFormComponent }, { path: 'verifiable', diff --git a/src/app/features/siop-custom/cbor-selectable.module.ts b/src/app/features/selectable-presentation/selectable-presentation.module.ts similarity index 72% rename from src/app/features/siop-custom/cbor-selectable.module.ts rename to src/app/features/selectable-presentation/selectable-presentation.module.ts index 40cf6ed..9377378 100644 --- a/src/app/features/siop-custom/cbor-selectable.module.ts +++ b/src/app/features/selectable-presentation/selectable-presentation.module.ts @@ -5,8 +5,8 @@ import { MatExpansionModule } from '@angular/material/expansion'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { SiopCustomRoutingModule } from './cbor-selectable-routing.module'; -import { CreateAScenarioComponent } from './components/create-a-scenario/create-a-scenario.component'; +import { SiopCustomRoutingModule } from './selectable-presentation-routing.module'; +import { SelectablePresentationFormComponent } from '@features/selectable-presentation/components/selectable-presentation-form/selectable-presentation-form.component'; import { HomeComponent } from './components/home/home.component'; import { SharedModule } from '@app/shared/shared.module'; import { WalletLayoutComponent } from '@app/core/layout/wallet-layout/wallet-layout.component'; @@ -15,7 +15,7 @@ import { HelperCborSelectableService } from './services/helper-cbor-selectable.s @NgModule({ declarations: [ - CreateAScenarioComponent, + SelectablePresentationFormComponent, HomeComponent ], imports: [ @@ -32,4 +32,4 @@ import { HelperCborSelectableService } from './services/helper-cbor-selectable.s HelperCborSelectableService ] }) -export class SiopCustomModule { } +export class SelectablePresentationModule { } diff --git a/src/app/features/siop-custom/services/helper-cbor-selectable.service.spec.ts b/src/app/features/selectable-presentation/services/helper-cbor-selectable.service.spec.ts similarity index 100% rename from src/app/features/siop-custom/services/helper-cbor-selectable.service.spec.ts rename to src/app/features/selectable-presentation/services/helper-cbor-selectable.service.spec.ts diff --git a/src/app/features/siop-custom/services/helper-cbor-selectable.service.ts b/src/app/features/selectable-presentation/services/helper-cbor-selectable.service.ts similarity index 100% rename from src/app/features/siop-custom/services/helper-cbor-selectable.service.ts rename to src/app/features/selectable-presentation/services/helper-cbor-selectable.service.ts diff --git a/src/app/features/siop-custom/models/Constraint.ts b/src/app/features/siop-custom/models/Constraint.ts deleted file mode 100644 index b093204..0000000 --- a/src/app/features/siop-custom/models/Constraint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { DefinitionPath } from './DefinitionPath'; - -export type Constraint = { - fields: DefinitionPath[] -} diff --git a/src/app/features/siop-custom/services/create-form.service.ts b/src/app/features/siop-custom/services/create-form.service.ts deleted file mode 100644 index 34a5759..0000000 --- a/src/app/features/siop-custom/services/create-form.service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@angular/core'; -import { FormGroup, FormArray, FormControl } from '@angular/forms'; -import { CBORFields } from '@app/core/data/cbor_fields'; - -@Injectable() -export class CreateFormService { - - form = new FormGroup({ - fields: new FormArray([]) - }); - constructor () { - CBORFields.forEach(() => this.items.push(new FormControl())); - } - get items (): FormArray { - return this.form.get('fields') as FormArray; - } -} diff --git a/src/favicon.ico b/src/favicon.ico index e3b0be622d69b2b73696390b4808225fe4b57a6e..ccf48c4392a806cc2c58a87d1a71389b5f6f0e59 100644 GIT binary patch literal 15406 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 literal 87710 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 aPH(@%`?o*Za%6Gb$CG$zqh?jR{{I25_gCux