Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,29 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 pb-4 dark:text-white">{{ 'UP
</div>-->
}
@if (isCurrentStep('compliance')) {
<div class="flex w-full justify-end items-center gap-2 px-4 mb-3">
<div class="flex w-full items-center px-4 mb-3">
<div class="flex items-center">
@if(complianceVC && complianceLevel=='BL'){
<img
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
src="assets/logos/baseline.png"
alt="Baseline logo"
/>
} @else if(complianceVC && complianceLevel=='P') {
<img
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
src="assets/logos/prof.png"
alt="Professional logo"
/>
} @else if(complianceVC && complianceLevel=='PP') {
<img
class="h-[1.9rem] max-w-[12rem] object-contain rounded-md"
src="assets/logos/profplus.png"
alt="Professional plus logo"
/>
}
</div>
<div class="ml-auto flex items-center gap-2">
<button type="button" (click)="openRequestValidationModal(); $event.stopPropagation();"
[disabled]="!hasSelfAttestation() || hasUnsavedComplianceProfileChanges()"
[ngClass]="(!hasSelfAttestation() || hasUnsavedComplianceProfileChanges()) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-primary-50'"
Expand All @@ -284,6 +306,7 @@ <h2 class="text-3xl font-bold text-primary-100 ml-4 pb-4 dark:text-white">{{ 'UP
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11h2v5m-2 0h4m-2.592-8.5h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
</svg>
</div>
</div>
</div>
@if (hasUnsavedComplianceProfileChanges()) {
<div class="mx-4 mb-3 p-3 text-sm text-amber-800 rounded-lg bg-amber-100 dark:bg-amber-900 dark:text-amber-200" role="alert">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { AttachmentServiceService } from 'src/app/services/attachment-service.se
import { ServiceSpecServiceService } from 'src/app/services/service-spec-service.service';
import { ResourceSpecServiceService } from 'src/app/services/resource-spec-service.service';
import { PaginationService } from 'src/app/services/pagination.service';
import { QrVerifierService } from 'src/app/services/qr-verifier.service';

class SyncFileReaderMock {
onload: ((event: any) => void) | null = null;
Expand All @@ -25,6 +24,13 @@ class SyncFileReaderMock {
}
}

const asJwt = (payload: any): string => {
const encode = (value: any) =>
btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');

return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.`;
};

describe('UpdateProductSpecComponent', () => {
let component: UpdateProductSpecComponent;
let fixture: ComponentFixture<UpdateProductSpecComponent>;
Expand All @@ -37,7 +43,6 @@ describe('UpdateProductSpecComponent', () => {
let attachmentServiceSpy: jasmine.SpyObj<AttachmentServiceService>;
let servSpecServiceSpy: jasmine.SpyObj<ServiceSpecServiceService>;
let resSpecServiceSpy: jasmine.SpyObj<ResourceSpecServiceService>;
let qrVerifierSpy: jasmine.SpyObj<QrVerifierService>;
let paginationServiceSpy: jasmine.SpyObj<PaginationService>;
let originalFileReader: any;

Expand Down Expand Up @@ -67,15 +72,12 @@ describe('UpdateProductSpecComponent', () => {
attachmentServiceSpy = jasmine.createSpyObj<AttachmentServiceService>('AttachmentServiceService', ['uploadFile']);
servSpecServiceSpy = jasmine.createSpyObj<ServiceSpecServiceService>('ServiceSpecServiceService', ['getServiceSpecByUser']);
resSpecServiceSpy = jasmine.createSpyObj<ResourceSpecServiceService>('ResourceSpecServiceService', ['getResourceSpecByUser']);
qrVerifierSpy = jasmine.createSpyObj<QrVerifierService>('QrVerifierService', ['launchPopup', 'pollCertCredential']);
paginationServiceSpy = jasmine.createSpyObj<PaginationService>('PaginationService', ['getItemsPaginated']);

localStorageSpy.getObject.and.returnValue({});
attachmentServiceSpy.uploadFile.and.returnValue(of({ content: 'https://uploaded.file' }));
prodSpecServiceSpy.getResSpecById.and.resolveTo({ id: 'rel-prod', name: 'Rel Prod' } as any);
prodSpecServiceSpy.updateProdSpec.and.returnValue(of({ id: 'created' }));
qrVerifierSpy.launchPopup.and.returnValue({} as Window);
qrVerifierSpy.pollCertCredential.and.resolveTo({ subject: { compliance: [] }, vc: 'vc-token' });
paginationServiceSpy.getItemsPaginated.and.resolveTo(defaultPaginationData);

await TestBed.configureTestingModule({
Expand All @@ -90,7 +92,6 @@ describe('UpdateProductSpecComponent', () => {
{ provide: AttachmentServiceService, useValue: attachmentServiceSpy },
{ provide: ServiceSpecServiceService, useValue: servSpecServiceSpy },
{ provide: ResourceSpecServiceService, useValue: resSpecServiceSpy },
{ provide: QrVerifierService, useValue: qrVerifierSpy },
{ provide: PaginationService, useValue: paginationServiceSpy }
]
}).compileComponents();
Expand Down Expand Up @@ -508,6 +509,33 @@ describe('UpdateProductSpecComponent', () => {
expect(component.prodChars[0].id).toBe('urn:ngsi-ld:characteristic:platinum-id');
});

it('populateProductInfo should decode Compliance:VC and expose compliance badge level', () => {
const vcToken = asJwt({
vc: {
credentialSubject: {
'gx:labelLevel': 'P'
}
}
});

component.prod = {
...component.prod,
productSpecCharacteristic: [
{
id: 'urn:ngsi-ld:characteristic:vc-id',
name: 'Compliance:VC',
productSpecCharacteristicValue: [{ isDefault: true, value: vcToken }]
}
]
} as any;

component.populateProductInfo();

expect(component.complianceVCId).toBe('urn:ngsi-ld:characteristic:vc-id');
expect(component.complianceVC).toBe(vcToken);
expect(component.complianceLevel).toBe('P');
});

it('hasUnsavedComplianceProfileChanges should return false when compliance profile matches persisted data', () => {
component.prod = {
...component.prod,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { NgxFileDropEntry, FileSystemFileEntry, FileSystemDirectoryEntry } from
import { certifications } from 'src/app/models/certification-standards.const'
import * as moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { QrVerifierService } from 'src/app/services/qr-verifier.service';
import { jwtDecode } from "jwt-decode";
import { noWhitespaceValidator } from 'src/app/validators/validators';
import { Subject } from 'rxjs';
Expand Down Expand Up @@ -114,6 +113,7 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
selectedISOS:any[]=[];
additionalISOS:any[]=[];
verifiedISO:string[] = [];
complianceLevel:string='NL';
selectedISO:any;
complianceVC:any = null;
complianceVCId:string = '';
Expand Down Expand Up @@ -213,7 +213,6 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
private attachmentService: AttachmentServiceService,
private servSpecService: ServiceSpecServiceService,
private resSpecService: ResourceSpecServiceService,
private qrVerifier: QrVerifierService,
private paginationService: PaginationService
) {
for(let i=0; i<certifications.length; i++){
Expand Down Expand Up @@ -300,26 +299,10 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
// Check if this is a VC
if (this.prod.productSpecCharacteristic[i].name == 'Compliance:VC') {
this.complianceVCId = this.prod.productSpecCharacteristic[i].id || '';
this.complianceVC = this.prod.productSpecCharacteristic[i].productSpecCharacteristicValue?.[0]?.value ?? null;
// Decode the token
try {
const decoded = jwtDecode(this.prod.productSpecCharacteristic[i].productSpecCharacteristicValue[0].value)
let credential: any = null

if ('verifiableCredential' in decoded) {
credential = decoded.verifiableCredential;
} else if('vc' in decoded) {
credential = decoded.vc;
}

if (credential != null) {
const subject = credential.credentialSubject;

if ('compliance' in subject) {
this.verifiedISO = subject.compliance.map((comp: any) => {
return comp.standard
})
}
}
this.applyComplianceDataFromVcToken(this.complianceVC);
} catch (e) {
console.log(e)
}
Expand Down Expand Up @@ -638,27 +621,36 @@ export class UpdateProductSpecComponent implements OnInit, OnDestroy {
console.log(this.selectedISOS)
}

verifyCredential() {
console.log('verifing credential')
const state = `cert:${uuidv4()}`
private applyComplianceDataFromVcToken(vcToken: any) {
if (!vcToken || typeof vcToken !== 'string') {
this.complianceLevel = 'NL';
return;
}

const allowedLevels = ['NL', 'BL', 'P', 'PP'];

const qrWin = this.qrVerifier.launchPopup(`${environment.SIOP_INFO.verifierHost}${environment.SIOP_INFO.verifierQRCodePath}?state=${state}&client_callback=${environment.SIOP_INFO.callbackURL}&client_id=${environment.SIOP_INFO.clientID}`, 'Scan QR code', 500, 500)
this.qrVerifier.pollCertCredential(qrWin, state).then((data) => {
// Process the VC to verify the cerficates
// Validate the product ID and company
const subject = data.subject
try {
const decoded: any = jwtDecode(vcToken);
let credential: any = null;

if (subject.compliance) {
subject.compliance.forEach((comp: any) => {
this.verifiedISO.push(comp.standard)
})
if ('verifiableCredential' in decoded) {
credential = decoded.verifiableCredential;
} else if ('vc' in decoded) {
credential = decoded.vc;
}

this.complianceVC = data.vc;
const subject = credential?.credentialSubject;
if (!subject) {
this.complianceLevel = 'NL';
return;
}

//this.verifiedISO[sel.name] = data.vc
console.log(`We got the vc: ${data['vc']}`)
})
const level = subject['gx:labelLevel'];
this.complianceLevel = (typeof level === 'string' && allowedLevels.includes(level)) ? level : 'NL';
} catch (error) {
this.complianceLevel = 'NL';
console.log(error);
}
}

openRequestValidationModal() {
Expand Down
Loading