Pull request #365: VM/RED-3982

Merge in RED/ui from VM/RED-3982 to master

* commit 'f888d59a660892e6f8759faf513c1117f969f8fa':
  RED-3982 - used 'DetailsRadioComponent' to choose between kms and pkcs configurations
  RED-3982 - updates to be able to edit/delete digital signature
  RED-3982 - changes to can update digital signature
  RED-3982 - fixed some imports
  RED-3982 - updates to add 'pkcs signature' and 'kms signature' forms also on main screen component to be edited
  RED-3982 - added 'pkcs signature' and 'kms signature' forms
  RED-3982 - extracted upload file logic from import redactions dialog component into a separate component to be reusable also for certificate configuration
  RED-3982 - WIP on 'Configure Digital Signature Certificate' dialog
This commit is contained in:
Valentin-Gabriel Mihai 2022-06-03 17:09:48 +02:00
commit 37ed602cba
36 changed files with 1081 additions and 731 deletions

View File

@ -43,6 +43,9 @@ import { ConfirmDeleteDossierStateDialogComponent } from './dialogs/confirm-dele
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { AdminSideNavComponent } from './admin-side-nav/admin-side-nav.component';
import { SystemPreferencesFormComponent } from './screens/general-config/system-preferences-form/system-preferences-form.component';
import { ConfigureCertificateDialogComponent } from './dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
import { PkcsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
const dialogs = [
AddEditCloneDossierTemplateDialogComponent,
@ -57,6 +60,7 @@ const dialogs = [
UploadDictionaryDialogComponent,
AddEditDossierStateDialogComponent,
ConfirmDeleteDossierStateDialogComponent,
ConfigureCertificateDialogComponent,
];
const screens = [
@ -84,6 +88,8 @@ const components = [
GeneralConfigFormComponent,
SmtpFormComponent,
SystemPreferencesFormComponent,
PkcsSignatureConfigurationComponent,
KmsSignatureConfigurationComponent,
...dialogs,
...screens,

View File

@ -0,0 +1,39 @@
<section class="dialog">
<div
class="dialog-header heading-l"
[translate]="!isInConfiguration ? translations.title.beforeConfiguration : translations.title[selectedOption]"
></div>
<div class="dialog-content">
<form [formGroup]="form" *ngIf="!isInConfiguration">
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>
</form>
<ng-container *ngIf="isInConfiguration">
{{ 'digital-signature-dialog.upload-warning-message' | translate }}
<redaction-pkcs-signature-configuration
*ngIf="selectedOption === digitalSignatureOptions.PKCS"
></redaction-pkcs-signature-configuration>
<redaction-kms-signature-configuration
*ngIf="selectedOption === digitalSignatureOptions.KMS"
></redaction-kms-signature-configuration>
</ng-container>
</div>
<div class="dialog-actions">
<ng-container *ngIf="!isInConfiguration">
<button (click)="toggleIsInConfiguration()" color="primary" mat-flat-button>
{{ 'digital-signature-dialog.actions.continue' | translate }}
</button>
<div translate="digital-signature-dialog.actions.cancel" class="all-caps-label cancel" mat-dialog-close></div>
</ng-container>
<ng-container *ngIf="isInConfiguration">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button>
{{ 'digital-signature-dialog.actions.save' | translate }}
</button>
<div translate="digital-signature-dialog.actions.back" class="all-caps-label cancel" (click)="toggleIsInConfiguration()"></div>
</ng-container>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -0,0 +1,37 @@
@use 'variables';
.dialog {
.dialog-content {
.option {
margin-top: 12px;
height: 56px;
border-radius: 8px;
background: rgba(variables.$grey-2, 0.8);
border: 16px solid transparent;
cursor: pointer;
.title {
display: flex;
align-items: center;
font-weight: bold;
iqser-round-checkbox {
padding-right: 6px;
}
p {
font-weight: bold;
}
}
.description {
font-size: 11px;
opacity: 0.7;
margin-top: 6px;
}
}
.selected {
background: rgba(variables.$red-1, 0.1);
}
}
}

View File

@ -0,0 +1,93 @@
import { ChangeDetectorRef, Component, Injector, ViewChild } from '@angular/core';
import { digitalSignatureDialogTranslations } from '../../translations/digital-signature-dialog-translations';
import { BaseDialogComponent, DetailsRadioOption, LoadingService, Toaster } from '@iqser/common-ui';
import { MatDialogRef } from '@angular/material/dialog';
import { PkcsSignatureConfigurationComponent } from './form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './form/kms-signature-configuration/kms-signature-configuration.component';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HttpStatusCode } from '@angular/common/http';
import { DigitalSignatureOption, DigitalSignatureOptions } from '@red/domain';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
const DEFAULT_DIALOG_WIDTH = '662px';
const KMS_SIGNATURE_DIALOG_WIDTH = '810px';
@Component({
templateUrl: './configure-certificate-dialog.component.html',
styleUrls: ['./configure-certificate-dialog.component.scss'],
})
export class ConfigureCertificateDialogComponent extends BaseDialogComponent {
@ViewChild(PkcsSignatureConfigurationComponent) pkcsSignatureConfigurationComponent: PkcsSignatureConfigurationComponent;
@ViewChild(KmsSignatureConfigurationComponent) kmsSignatureConfigurationComponent: KmsSignatureConfigurationComponent;
readonly digitalSignatureOptions = DigitalSignatureOptions;
readonly translations = digitalSignatureDialogTranslations;
readonly options: DetailsRadioOption<DigitalSignatureOption>[] = [
{
label: _('digital-signature-dialog.options.pkcs.label'),
value: DigitalSignatureOptions.PKCS,
description: _('digital-signature-dialog.options.pkcs.description'),
},
{
label: _('digital-signature-dialog.options.kms.label'),
value: DigitalSignatureOptions.KMS,
description: _('digital-signature-dialog.options.kms.description'),
},
];
isInConfiguration = false;
constructor(
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ConfigureCertificateDialogComponent>,
private readonly _formBuilder: FormBuilder,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
private readonly _changeDetectorRef: ChangeDetectorRef,
) {
super(_injector, _dialogRef);
this.form = this._formBuilder.group({
option: [this.options[0]],
});
}
toggleIsInConfiguration() {
this.isInConfiguration = !this.isInConfiguration;
if (this.isInConfiguration && this.selectedOption === DigitalSignatureOptions.KMS) {
this._dialogRef.updateSize(KMS_SIGNATURE_DIALOG_WIDTH);
} else {
this._dialogRef.updateSize(DEFAULT_DIALOG_WIDTH);
}
this._changeDetectorRef.detectChanges();
}
get disabled(): boolean {
return this.activeComponent?.disabled;
}
get activeComponent() {
return this.selectedOption === DigitalSignatureOptions.PKCS
? this.pkcsSignatureConfigurationComponent
: this.kmsSignatureConfigurationComponent;
}
get selectedOption(): DigitalSignatureOption {
return this.form.get('option').value.value;
}
async save(): Promise<void> {
try {
await this.activeComponent.save();
this._toaster.success(_('digital-signature-dialog.actions.save-success'));
this._dialogRef.close(true);
} catch (error) {
console.log(error);
if (error.status === HttpStatusCode.BadRequest) {
this._toaster.error(_('digital-signature-dialog.actions.certificate-not-valid-error'));
} else {
this._toaster.error(_('digital-signature-dialog.actions.save-error'));
}
}
}
}

View File

@ -0,0 +1,50 @@
import { BaseFormComponent } from '@iqser/common-ui';
import { DigitalSignatureService } from '../../../services/digital-signature.service';
import { IDigitalSignatureRequest, DigitalSignatureOption, DigitalSignatureOptions } from '@red/domain';
import { firstValueFrom, Observable } from 'rxjs';
export abstract class BaseSignatureConfigurationComponent extends BaseFormComponent {
file: File | null;
protected constructor(
protected readonly _digitalSignatureService: DigitalSignatureService,
private readonly _selectedOption: DigitalSignatureOption,
) {
super();
}
setCertificateName(file: File | null): void {
if (file) {
let name = file.name.split('.')[0];
name = name.replace(/-/g, ' ');
this.form.controls['certificateName'].setValue(name);
} else {
this.form.controls['certificateName'].setValue('');
}
}
generateFile(certificateName: string, extension: '.p12' | '.pem'): File | null {
if (certificateName) {
return {
name: certificateName.split(' ').join('-') + extension,
} as File;
}
return null;
}
resetInitialFormValue(): void {
this.initialFormValue = this.form.getRawValue();
}
deleteSignature(): Promise<unknown> {
const observable: Observable<unknown> =
this._selectedOption === DigitalSignatureOptions.PKCS
? this._digitalSignatureService.deleteSignature()
: this._digitalSignatureService.deleteKmsSignature();
return firstValueFrom(observable);
}
abstract addRemoveCertificate(file: File | null): void;
abstract save(): Promise<IDigitalSignatureRequest | unknown>;
}

View File

@ -0,0 +1,39 @@
<iqser-upload-file [class.w-300]="!!file" accept=".pem" [file]="file" [readonly]="!!file" (fileChanged)="addRemoveCertificate($event)">
</iqser-upload-file>
<form [formGroup]="form">
<div class="flex">
<div class="flex fields-container">
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.kms.certificate-name"></label>
<input formControlName="certificateName" type="text" />
</div>
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.kms.kms-service-endpoint"></label>
<input formControlName="kmsServiceEndpoint" type="text" />
</div>
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.kms.kms-region"></label>
<input formControlName="kmsRegion" type="text" />
</div>
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.kms.kms-id"></label>
<input formControlName="kmsKeyId" type="text" />
</div>
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.kms.kms-access-key"></label>
<input formControlName="kmsAccessKey" type="text" />
</div>
<div class="iqser-input-group required w-300" *ngIf="!digitalSignature">
<label translate="digital-signature-dialog.forms.kms.kms-secret-key"></label>
<input formControlName="kmsSecretKey" type="text" />
</div>
</div>
<div class="flex fields-container" *ngIf="!digitalSignature">
<div class="iqser-input-group required w-400 certificate">
<label translate="digital-signature-dialog.forms.kms.certificate-content"></label>
<textarea formControlName="certificate" type="text"></textarea>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,18 @@
.certificate {
height: 100%;
textarea {
resize: none;
height: 100%;
}
}
.w-300 {
width: 300px;
}
iqser-upload-file {
display: block;
margin-top: 24px;
margin-bottom: 24px;
}

View File

@ -0,0 +1,59 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { BaseSignatureConfigurationComponent } from '../base-signature-configuration-component';
import { IKmsDigitalSignature, IKmsDigitalSignatureRequest, DigitalSignatureOptions } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { DigitalSignatureService } from '../../../../services/digital-signature.service';
@Component({
selector: 'redaction-kms-signature-configuration',
templateUrl: './kms-signature-configuration.component.html',
styleUrls: ['./kms-signature-configuration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class KmsSignatureConfigurationComponent extends BaseSignatureConfigurationComponent implements OnInit {
@Input() digitalSignature!: IKmsDigitalSignatureRequest;
constructor(protected readonly _digitalSignatureService: DigitalSignatureService, private readonly _formBuilder: FormBuilder) {
super(_digitalSignatureService, DigitalSignatureOptions.KMS);
}
ngOnInit() {
this.form = this._formBuilder.group({
certificateName: [this.digitalSignature?.certificateName, Validators.required],
kmsServiceEndpoint: [this.digitalSignature?.kmsServiceEndpoint, Validators.required],
kmsRegion: [this.digitalSignature?.kmsRegion, Validators.required],
kmsKeyId: [this.digitalSignature?.kmsKeyId, Validators.required],
kmsAccessKey: [this.digitalSignature?.kmsAccessKey, Validators.required],
kmsSecretKey: this.digitalSignature ? null : ['', Validators.required],
certificate: this.digitalSignature ? null : ['', Validators.required],
});
this.initialFormValue = this.form.getRawValue();
this.file = this.generateFile(this.digitalSignature?.certificateName, '.pem');
}
addRemoveCertificate(file: File | null): void {
this.setCertificateName(file);
if (file) {
const fileReader = new FileReader();
fileReader.onload = () => {
const binaryContent = <string>fileReader.result;
this.form.get('certificate').setValue(binaryContent);
};
fileReader.readAsBinaryString(file as Blob);
} else {
this.form.controls['certificate'].setValue('');
}
}
save(): Promise<IKmsDigitalSignatureRequest> {
const formValue = this.form.getRawValue();
const digitalSignature: IKmsDigitalSignature = { ...formValue };
if (!this.digitalSignature) {
digitalSignature.certificate = window.btoa(<string>digitalSignature.certificate);
}
return firstValueFrom(this._digitalSignatureService.saveKmsSignature(digitalSignature));
}
}

View File

@ -0,0 +1,33 @@
<iqser-upload-file
[class.w-400]="!!file"
accept=".p12"
[file]="file"
[readonly]="!!file"
(fileChanged)="addRemoveCertificate($event)"
></iqser-upload-file>
<form [formGroup]="form">
<div class="flex">
<div class="flex fields-container">
<div class="iqser-input-group required w-300">
<label translate="digital-signature-dialog.forms.pkcs.certificate-name"></label>
<input formControlName="certificateName" type="text" />
</div>
<div class="iqser-input-group required w-300" *ngIf="!digitalSignature">
<label translate="digital-signature-dialog.forms.pkcs.password-key"></label>
<input formControlName="password" type="password" />
</div>
<div class="iqser-input-group w-300">
<label translate="digital-signature-dialog.forms.pkcs.contact-information"></label>
<input formControlName="contactInfo" type="text" />
</div>
<div class="iqser-input-group w-300">
<label translate="digital-signature-dialog.forms.pkcs.location"></label>
<input formControlName="location" type="text" />
</div>
<div class="iqser-input-group w-450">
<label translate="digital-signature-dialog.forms.pkcs.reason"></label>
<textarea formControlName="reason" rows="4" type="text"></textarea>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,13 @@
textarea {
resize: none;
}
.w-400 {
width: 400px;
}
iqser-upload-file {
display: block;
margin-top: 24px;
margin-bottom: 24px;
}

View File

@ -0,0 +1,59 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { BaseSignatureConfigurationComponent } from '../base-signature-configuration-component';
import { IPkcsDigitalSignature, IPkcsDigitalSignatureRequest, DigitalSignatureOptions } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { DigitalSignatureService } from '../../../../services/digital-signature.service';
import { lastIndexOfEnd } from '../../../../../../utils';
@Component({
selector: 'redaction-pkcs-signature-configuration',
templateUrl: './pkcs-signature-configuration.component.html',
styleUrls: ['./pkcs-signature-configuration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PkcsSignatureConfigurationComponent extends BaseSignatureConfigurationComponent implements OnInit {
@Input() digitalSignature!: IPkcsDigitalSignatureRequest;
constructor(protected readonly _digitalSignatureService: DigitalSignatureService, private readonly _formBuilder: FormBuilder) {
super(_digitalSignatureService, DigitalSignatureOptions.PKCS);
}
ngOnInit() {
this.form = this._formBuilder.group({
certificateName: [this.digitalSignature?.certificateName, Validators.required],
password: this.digitalSignature ? null : ['', Validators.required],
contactInfo: [this.digitalSignature?.contactInfo],
location: [this.digitalSignature?.location],
reason: [this.digitalSignature?.reason],
base64EncodedPrivateKey: this.digitalSignature ? null : ['', Validators.required],
});
this.resetInitialFormValue();
this.file = this.generateFile(this.digitalSignature?.certificateName, '.p12');
}
addRemoveCertificate(file: File | null): void {
this.setCertificateName(file);
if (file) {
const fileReader = new FileReader();
fileReader.onload = () => {
const dataUrl = <string>fileReader.result;
const actualBase64Value = dataUrl.substring(lastIndexOfEnd(dataUrl, ';base64,'));
this.form.get('base64EncodedPrivateKey').setValue(actualBase64Value);
};
fileReader.readAsDataURL(file as Blob);
} else {
this.form.controls['base64EncodedPrivateKey'].setValue('');
}
}
save(): Promise<IPkcsDigitalSignatureRequest | unknown> {
const formValue = this.form.getRawValue();
const digitalSignature: IPkcsDigitalSignature = { ...formValue };
const observable = this.digitalSignature
? this._digitalSignatureService.updateSignature(digitalSignature)
: this._digitalSignatureService.saveSignature(digitalSignature);
return firstValueFrom(observable);
}
}

View File

@ -13,91 +13,42 @@
<div class="content-inner">
<div class="content-container">
<div class="content-container-content">
<form *ngIf="form" [formGroup]="form" autocomplete="off">
<input #fileInput (change)="fileChanged($event, fileInput)" class="file-upload-input" hidden type="file" />
<iqser-empty-state
(action)="openConfigureCertificate()"
*ngIf="!digitalSignature"
[buttonLabel]="'digital-signature-screen.no-data.action' | translate"
[text]="'digital-signature-screen.no-data.title' | translate"
icon="iqser:document"
[buttonIcon]="null"
></iqser-empty-state>
<iqser-empty-state
(action)="fileInput.click()"
*ngIf="!hasDigitalSignatureSet"
[buttonLabel]="'digital-signature-screen.no-data.action' | translate"
[text]="'digital-signature-screen.no-data.title' | translate"
buttonIcon="iqser:upload"
></iqser-empty-state>
<ng-container *ngIf="digitalSignature">
<redaction-pkcs-signature-configuration
*ngIf="currentCertificateType === certificateType.PKCS"
[digitalSignature]="digitalSignature"
></redaction-pkcs-signature-configuration>
<redaction-kms-signature-configuration
*ngIf="currentCertificateType === certificateType.KMS"
[digitalSignature]="digitalSignature"
></redaction-kms-signature-configuration>
</ng-container>
<div [class.hidden]="!hasDigitalSignatureSet" class="iqser-input-group required w-300">
<label translate="digital-signature-screen.certificate-name.label"></label>
<input
[placeholder]="'digital-signature-screen.certificate-name.placeholder' | translate"
formControlName="certificateName"
name="certificateName"
/>
</div>
<div [class.hidden]="!digitalSignature" class="changes-box">
<iqser-icon-button
(action)="saveDigitalSignature()"
[disabled]="disabled"
[label]="'digital-signature-screen.action.save' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<div
*ngIf="!digitalSignatureExists"
[class.hidden]="!hasDigitalSignatureSet"
class="iqser-input-group required w-300"
>
<label translate="digital-signature-screen.password.label"></label>
<input
[placeholder]="'digital-signature-screen.password.placeholder' | translate"
formControlName="keySecret"
name="keySecret"
/>
</div>
<div [class.hidden]="!hasDigitalSignatureSet" class="iqser-input-group w-300">
<label translate="digital-signature-screen.reason.label"></label>
<input
[placeholder]="'digital-signature-screen.reason.placeholder' | translate"
formControlName="reason"
name="reason"
/>
</div>
<div [class.hidden]="!hasDigitalSignatureSet" class="iqser-input-group w-300">
<label translate="digital-signature-screen.location.label"></label>
<input
[placeholder]="'digital-signature-screen.location.placeholder' | translate"
formControlName="location"
name="location"
/>
</div>
<div [class.hidden]="!hasDigitalSignatureSet" class="iqser-input-group w-300">
<label translate="digital-signature-screen.contact-info.label"></label>
<input
[placeholder]="'digital-signature-screen.contact-info.placeholder' | translate"
formControlName="contactInfo"
name="contactInfo"
/>
</div>
<div [class.hidden]="!hasDigitalSignatureSet" class="changes-box">
<iqser-icon-button
(action)="saveDigitalSignature()"
[disabled]="form.invalid"
[label]="'digital-signature-screen.action.save' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:check"
></iqser-icon-button>
<iqser-icon-button
(action)="removeDigitalSignature()"
*ngIf="digitalSignatureExists"
[label]="'digital-signature-screen.action.delete' | translate"
[type]="iconButtonTypes.primary"
icon="iqser:trash"
></iqser-icon-button>
<div
(click)="loadDigitalSignatureAndInitializeForm()"
*ngIf="!digitalSignatureExists"
class="all-caps-label cancel"
translate="digital-signature-screen.action.reset"
></div>
</div>
</form>
(click)="removeDigitalSignature()"
*ngIf="digitalSignature"
class="all-caps-label cancel"
translate="digital-signature-screen.action.remove"
></div>
</div>
</div>
</div>
</div>

View File

@ -1,14 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { lastIndexOfEnd } from '@utils/functions';
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
import { DigitalSignatureService } from '../../services/digital-signature.service';
import { IDigitalSignature } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { HttpStatusCode } from '@angular/common/http';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { PkcsSignatureConfigurationComponent } from '../../dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from '../../dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
import { DigitalSignatureOptions, IKmsDigitalSignatureRequest, IPkcsDigitalSignatureRequest } from '@red/domain';
@Component({
selector: 'redaction-digital-signature-screen',
@ -16,65 +16,34 @@ import { HttpStatusCode } from '@angular/common/http';
styleUrls: ['./digital-signature-screen.component.scss'],
})
export class DigitalSignatureScreenComponent implements OnInit {
@ViewChild(PkcsSignatureConfigurationComponent) pkcsSignatureConfigurationComponent: PkcsSignatureConfigurationComponent;
@ViewChild(KmsSignatureConfigurationComponent) kmsSignatureConfigurationComponent: KmsSignatureConfigurationComponent;
readonly certificateType = DigitalSignatureOptions;
readonly iconButtonTypes = IconButtonTypes;
readonly currentUser = this._userService.currentUser;
digitalSignature: IDigitalSignature;
form: FormGroup;
digitalSignatureExists = false;
digitalSignature: IPkcsDigitalSignatureRequest | IKmsDigitalSignatureRequest;
constructor(
private readonly _toaster: Toaster,
private readonly _formBuilder: FormBuilder,
private readonly _userService: UserService,
private readonly _loadingService: LoadingService,
readonly routerHistoryService: RouterHistoryService,
private readonly _digitalSignatureService: DigitalSignatureService,
private readonly _dialogService: AdminDialogService,
private readonly _changeDetectorRef: ChangeDetectorRef,
readonly routerHistoryService: RouterHistoryService,
) {}
get hasDigitalSignatureSet() {
return this.digitalSignatureExists || !!this.form.get('base64EncodedPrivateKey').value;
}
async ngOnInit(): Promise<void> {
await this.loadDigitalSignatureAndInitializeForm();
}
async saveDigitalSignature(): Promise<void> {
this._loadingService.start();
const formValue = this.form.getRawValue();
const digitalSignature: IDigitalSignature = {
...formValue,
};
//adjusted for chrome auto-complete / password manager
digitalSignature.password = formValue.keySecret;
const observable = this.digitalSignatureExists
? this._digitalSignatureService.update(digitalSignature)
: this._digitalSignatureService.save(digitalSignature);
try {
await firstValueFrom(observable);
await this.loadDigitalSignatureAndInitializeForm();
this._toaster.success(_('digital-signature-screen.action.save-success'));
} catch (error) {
console.error(error);
if (error.status === HttpStatusCode.BadRequest) {
this._toaster.error(_('digital-signature-screen.action.certificate-not-valid-error'));
} else {
this._toaster.error(_('digital-signature-screen.action.save-error'));
}
}
this._loadingService.stop();
await this.loadDigitalSignature();
}
async removeDigitalSignature(): Promise<void> {
this._loadingService.start();
try {
await firstValueFrom(this._digitalSignatureService.delete());
await this.loadDigitalSignatureAndInitializeForm();
await this.activeComponent.deleteSignature();
await this.loadDigitalSignature();
this._toaster.success(_('digital-signature-screen.action.delete-success'));
} catch (error) {
console.error(error);
@ -82,44 +51,47 @@ export class DigitalSignatureScreenComponent implements OnInit {
}
}
fileChanged(event, input: HTMLInputElement) {
const file = event.target.files[0];
const fileReader = new FileReader();
fileReader.onload = () => {
const dataUrl = <string>fileReader.result;
const actualBase64Value = dataUrl.substring(lastIndexOfEnd(dataUrl, ';base64,'));
this.form.get('base64EncodedPrivateKey').setValue(actualBase64Value);
this.form.get('certificateName').setValue(file.name);
input.value = null;
};
fileReader.readAsDataURL(file as Blob);
get disabled(): boolean {
//TODO remove second check when the update endpoint will be available for KMS signature
return this.activeComponent?.disabled || this.currentCertificateType === DigitalSignatureOptions.KMS;
}
async loadDigitalSignatureAndInitializeForm(): Promise<void> {
this._loadingService.start();
async saveDigitalSignature(): Promise<void> {
try {
const digitalSignature = await firstValueFrom(this._digitalSignatureService.getSignature());
this.digitalSignatureExists = true;
this.digitalSignature = digitalSignature;
await this.activeComponent.save();
this.activeComponent.resetInitialFormValue();
this._toaster.success(_('digital-signature-screen.action.save-success'));
} catch (error) {
this.digitalSignatureExists = false;
this.digitalSignature = {};
this._toaster.error(_('digital-signature-screen.action.save-error'));
}
this.form = this._getForm();
this._loadingService.stop();
}
private _getForm(): FormGroup {
return this._formBuilder.group({
certificateName: [this.digitalSignature.certificateName, Validators.required],
contactInfo: this.digitalSignature.contactInfo,
location: this.digitalSignature.location,
keySecret: this.digitalSignatureExists ? null : [this.digitalSignature.password, Validators.required],
reason: this.digitalSignature.reason,
base64EncodedPrivateKey: this.digitalSignatureExists
? null
: [this.digitalSignature.base64EncodedPrivateKey, Validators.required],
get currentCertificateType() {
if (!this.digitalSignature) {
return;
}
return 'contactInfo' in this.digitalSignature ? DigitalSignatureOptions.PKCS : DigitalSignatureOptions.KMS;
}
get activeComponent() {
return this.currentCertificateType === DigitalSignatureOptions.PKCS
? this.pkcsSignatureConfigurationComponent
: this.kmsSignatureConfigurationComponent;
}
openConfigureCertificate(): void {
const dialogRef = this._dialogService.openDialog('configureCertificate', null, null);
firstValueFrom(dialogRef.afterClosed()).then(async res => {
if (res) {
await this.loadDigitalSignature();
}
});
}
async loadDigitalSignature(): Promise<void> {
this._loadingService.start();
this.digitalSignature = await firstValueFrom(this._digitalSignatureService.getSignature());
this._loadingService.stop();
this._changeDetectorRef.detectChanges();
}
}

View File

@ -28,6 +28,7 @@ import { ActiveDossiersService } from '@services/dossiers/active-dossiers.servic
import { UserService } from '@services/user.service';
import { IDossierAttributeConfig, IFileAttributeConfig, IReportTemplate } from '@red/domain';
import { ReportTemplateService } from '@services/report-template.service';
import { ConfigureCertificateDialogComponent } from '../dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
type DialogType =
| 'confirm'
@ -42,7 +43,8 @@ type DialogType =
| 'addEditDossierAttribute'
| 'uploadDictionary'
| 'addEditDossierState'
| 'deleteDossierState';
| 'deleteDossierState'
| 'configureCertificate';
@Injectable()
export class AdminDialogService extends DialogService<DialogType> {
@ -95,6 +97,10 @@ export class AdminDialogService extends DialogService<DialogType> {
deleteDossierState: {
component: ConfirmDeleteDossierStateDialogComponent,
},
configureCertificate: {
component: ConfigureCertificateDialogComponent,
dialogConfig: { disableClose: false, maxHeight: '100vh' },
},
};
constructor(

View File

@ -1,7 +1,14 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService, RequiredParam, Validate } from '@iqser/common-ui';
import { Observable } from 'rxjs';
import { IDigitalSignature, IDigitalSignatureRequest } from '@red/domain';
import { filterEach, GenericService, RequiredParam, Validate } from '@iqser/common-ui';
import { filter, forkJoin, Observable, of } from 'rxjs';
import {
IDigitalSignatureRequest,
IKmsDigitalSignature,
IKmsDigitalSignatureRequest,
IPkcsDigitalSignature,
IPkcsDigitalSignatureRequest,
} from '@red/domain';
import { catchError, map, tap } from 'rxjs/operators';
@Injectable()
export class DigitalSignatureService extends GenericService<IDigitalSignatureRequest> {
@ -10,20 +17,34 @@ export class DigitalSignatureService extends GenericService<IDigitalSignatureReq
}
@Validate()
update(@RequiredParam() body: IDigitalSignatureRequest): Observable<unknown> {
updateSignature(@RequiredParam() body: IPkcsDigitalSignatureRequest): Observable<unknown> {
return this._put(body);
}
@Validate()
save(@RequiredParam() body: IDigitalSignature): Observable<IDigitalSignatureRequest> {
saveSignature(@RequiredParam() body: IPkcsDigitalSignature): Observable<IPkcsDigitalSignatureRequest> {
return this._post(body);
}
delete(): Observable<unknown> {
@Validate()
saveKmsSignature(@RequiredParam() body: IKmsDigitalSignature): Observable<IKmsDigitalSignatureRequest> {
return this._post(body, `${this._defaultModelPath}/kms`);
}
deleteSignature(): Observable<unknown> {
return super.delete({});
}
deleteKmsSignature(): Observable<unknown> {
return super.delete({}, `${this._defaultModelPath}/kms`);
}
getSignature(): Observable<IDigitalSignatureRequest> {
return super.getAll();
const digitalSignature$ = super.getAll().pipe(catchError(() => of(null)));
const kmsDigitalSignature$ = super.getAll(`${this._defaultModelPath}/kms`).pipe(catchError(() => of(null)));
return forkJoin([digitalSignature$, kmsDigitalSignature$]).pipe(
filterEach(signature => !!signature),
map(signatures => signatures[0] as IDigitalSignatureRequest),
);
}
}

View File

@ -0,0 +1,9 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export const digitalSignatureDialogTranslations = {
title: {
beforeConfiguration: _('digital-signature-dialog.title.before-configuration'),
pkcs: _('digital-signature-dialog.title.pkcs'),
kms: _('digital-signature-dialog.title.kms'),
},
} as const;

View File

@ -3,37 +3,21 @@
<div class="dialog-content">
<div translate="import-redactions-dialog.details" class="mb-24"></div>
<div
class="upload-area"
*ngIf="!fileToImport"
(click)="triggerAttachFile()"
redactionDragDropFileUpload
(fileDropped)="attachFile($event)"
>
<mat-icon svgIcon="iqser:upload"></mat-icon>
<div translate="import-redactions-dialog.upload-area-text"></div>
</div>
<ng-container *ngIf="fileToImport">
<div class="file-area">
<mat-icon svgIcon="iqser:document"></mat-icon>
<p>{{ fileToImport.name }}</p>
<mat-icon svgIcon="iqser:trash" (click)="removeFile()"></mat-icon>
</div>
<div class="only-for-pages">
<mat-checkbox
(change)="onlyForSpecificPages = !onlyForSpecificPages"
[checked]="onlyForSpecificPages"
class="filter-menu-checkbox"
color="primary"
>
{{ 'import-redactions-dialog.only-for-specific-pages' | translate }}
</mat-checkbox>
<iqser-upload-file (fileChanged)="fileChanged($event)"></iqser-upload-file>
<div class="only-for-pages" *ngIf="fileToImport">
<mat-checkbox
(change)="onlyForSpecificPages = !onlyForSpecificPages"
[checked]="onlyForSpecificPages"
class="filter-menu-checkbox"
color="primary"
>
{{ 'import-redactions-dialog.only-for-specific-pages' | translate }}
</mat-checkbox>
<div *ngIf="onlyForSpecificPages" class="iqser-input-group datepicker-wrapper">
<input />
</div>
<div *ngIf="onlyForSpecificPages" class="iqser-input-group datepicker-wrapper">
<input />
</div>
</ng-container>
</div>
</div>
<div class="dialog-actions">
@ -45,5 +29,3 @@
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>
<input #attachFileInput [hidden]="true" (change)="attachFile($event)" class="file-upload-input" type="file" accept="application/pdf" />

View File

@ -1,62 +1,3 @@
@use 'variables';
.upload-area,
.file-area {
display: flex;
align-items: center;
border-radius: 8px;
width: 586px;
background: variables.$grey-2;
}
.upload-area {
gap: 16px;
height: 88px;
cursor: pointer;
mat-icon,
div {
opacity: 0.5;
transition: 0.1s;
}
mat-icon {
margin-left: 32px;
}
div {
font-size: 16px;
font-weight: 500;
}
}
.file-area {
gap: 10px;
height: 48px;
mat-icon:first-child {
opacity: 0.5;
margin-left: 16px;
}
mat-icon:last-child {
margin-left: auto;
margin-right: 16px;
cursor: pointer;
}
mat-icon {
transform: scale(0.7);
}
p {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 490px;
}
}
.only-for-pages {
margin-top: 16px;
margin-left: 21px;

View File

@ -33,28 +33,13 @@ export class ImportRedactionsDialogComponent extends BaseDialogComponent {
super(_injector, _dialogRef);
}
triggerAttachFile() {
this.attachFileInput.nativeElement.click();
}
attachFile(event) {
const files = event.target['files'];
this.fileToImport = files[0];
// input field needs to be set as empty in case the same file will be selected second time
event.target.value = '';
fileChanged(file: File | null) {
this.fileToImport = file;
if (!this.fileToImport) {
console.error('No file to import!');
return;
this.onlyForSpecificPages = false;
}
}
removeFile() {
this.fileToImport = null;
this.onlyForSpecificPages = false;
}
async save(): Promise<void> {
this._loadingService.start();
const import$ = this._redactionImportService.importRedactions(this.data.dossierId, this.data.fileId, this.fileToImport);

View File

@ -24,14 +24,6 @@
padding: 0;
}
.fields-container {
flex-direction: column;
&:first-child {
margin-right: 40px;
}
}
redaction-small-chip {
margin-right: 8px;
}

View File

@ -1,41 +0,0 @@
import { Directive, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
const DRAG_OVER_BACKGROUND_COLOR = '#e2eefd';
const DEFAULT_BACKGROUND_COLOR = '#f4f5f7';
@Directive({
selector: '[redactionDragDropFileUpload]',
})
export class DragDropFileUploadDirective {
@Output() readonly fileDropped = new EventEmitter<any>();
@HostBinding('style.background-color') private background = DEFAULT_BACKGROUND_COLOR;
@HostListener('dragover', ['$event'])
onDragOver(event) {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer.types.includes('Files')) {
this.background = DRAG_OVER_BACKGROUND_COLOR;
}
}
@HostListener('dragleave', ['$event'])
onDragLeave(event) {
event.preventDefault();
event.stopPropagation();
this.background = DEFAULT_BACKGROUND_COLOR;
}
@HostListener('drop', ['$event'])
onDrop(event) {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer.types.includes('Files')) {
this.background = DEFAULT_BACKGROUND_COLOR;
const files = event.dataTransfer.files;
if (files.length > 0) {
this.fileDropped.emit({ target: { files } });
}
}
}
}

View File

@ -30,7 +30,6 @@ import { FileStatsComponent } from './components/file-stats/file-stats.component
import { FileNameColumnComponent } from '@shared/components/file-name-column/file-name-column.component';
import { DossierNameColumnComponent } from '@shared/components/dossier-name-column/dossier-name-column.component';
import { MAT_DATE_FORMATS } from '@angular/material/core';
import { DragDropFileUploadDirective } from '@shared/directives/drag-drop-file-upload.directive';
import { DossiersTypeSwitchComponent } from '@shared/components/dossiers-type-switch/dossiers-type-switch.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
@ -62,7 +61,7 @@ const components = [
...buttons,
];
const utils = [DatePipe, NamePipe, NavigateLastDossiersScreenDirective, LongPressDirective, DragDropFileUploadDirective];
const utils = [DatePipe, NamePipe, NavigateLastDossiersScreenDirective, LongPressDirective];
const services = [SharedDialogService];

View File

@ -648,40 +648,64 @@
}
},
"digital-signature": "Digitale Signatur",
"digital-signature-dialog": {
"actions": {
"back": "",
"cancel": "",
"certificate-not-valid-error": "",
"continue": "",
"save": "",
"save-error": "",
"save-success": ""
},
"forms": {
"kms": {
"certificate-content": "",
"certificate-name": "",
"kms-access-key": "",
"kms-id": "",
"kms-region": "",
"kms-secret-key": "",
"kms-service-endpoint": ""
},
"pkcs": {
"certificate-name": "",
"contact-information": "",
"location": "",
"password-key": "",
"reason": ""
}
},
"options": {
"kms": {
"description": "",
"label": ""
},
"pkcs": {
"description": "",
"label": ""
}
},
"title": {
"before-configuration": "",
"kms": "",
"pkcs": ""
},
"upload-warning-message": ""
},
"digital-signature-screen": {
"action": {
"certificate-not-valid-error": "Das hochgeladene Zertifikat eignet sich nicht zum Signieren von PDF-Dokumenten. Sie benötigen das Format PCKS#12.",
"delete": "Digitale Signatur löschen",
"certificate-not-valid-error": "",
"delete-error": "Die digitale Signatur konnte nicht entfernt werden, bitte versuchen Sie es erneut.",
"delete-success": "Die digitale Signatur wurde gelöscht. Geschwärzte Dateien werden nicht länger mit einer Signatur versehen!",
"reset": "Zurücksetzen",
"remove": "",
"save": "Digitale Signatur speichern",
"save-error": "Fehler beim Speichern der digitalen Signatur",
"save-success": "Digitale Signatur erfolgreich gespeichert"
},
"certificate-name": {
"label": "Name des Zertifikats",
"placeholder": "Name des Zertifikats"
},
"contact-info": {
"label": "Kontaktdaten",
"placeholder": "Kontaktdaten"
},
"location": {
"label": "Ort",
"placeholder": "Ort"
"save-error": "",
"save-success": ""
},
"no-data": {
"action": "Zertifikat hochladen",
"title": "Es ist kein Zertifikat für die digitale Signatur konfiguriert. Laden Sie ein PCKS#12-Zertifikat hoch, um Ihre geschwärzten Dokumente zu signieren."
},
"password": {
"label": "Zertifikatspasswort/-schlüssel",
"placeholder": "Passwort"
},
"reason": {
"label": "Begründung",
"placeholder": "Begründung"
}
},
"document-info": {

View File

@ -648,40 +648,63 @@
}
},
"digital-signature": "Digital Signature",
"digital-signature-dialog": {
"actions": {
"back": "Back",
"cancel": "Cancel",
"certificate-not-valid-error": "Uploaded Certificate is not valid!",
"continue": "Continue",
"save": "Save Configurations",
"save-error": "Failed to save digital signature!",
"save-success": "Digital Signature Certificate successfully saved!"
},
"forms": {
"kms": {
"certificate-content": "Certificate Content",
"certificate-name": "Certificate Name",
"kms-access-key": "KMS Access Key",
"kms-id": "KMS Id",
"kms-region": "KMS Region",
"kms-secret-key": "KMS Secret Key",
"kms-service-endpoint": "KMS Service Endpoint"
},
"pkcs": {
"certificate-name": "Certificate Name",
"contact-information": "Contact Information",
"location": "Location",
"password-key": "Password Key",
"reason": "Reason"
}
},
"options": {
"kms": {
"description": "Provide a corresponding PEM file containing the certificate, along with Amazon KMS credentials needed for securing the private key.",
"label": "I use an Amazon KMS private key"
},
"pkcs": {
"description": "A PKCS#12 file is a file that bundles the private key and the X.509 certificate. The password protection is required to secure the private key. Unprotected PKCS#12 files are not supported.",
"label": "I want to upload a PKCS#12 file"
}
},
"title": {
"before-configuration": "Configure Digital Signature Certificate",
"kms": "Configure a Certificate with Amazon KMS",
"pkcs": "Configure a PKCS#12 Certificate"
},
"upload-warning-message": "To configure the certificate, you first need to upload it."
},
"digital-signature-screen": {
"action": {
"certificate-not-valid-error": "Uploaded Certificate is not valid for signing PDFs. PCKS.12 format is required.",
"delete": "Delete Digital Signature",
"delete-error": "Failed to remove digital signature, please try again.",
"delete-success": "Digital signature removed. Redacted files will no longer be signed!",
"reset": "Reset",
"save": "Save Digital Signature",
"save-error": "Failed to save digital signature",
"save-success": "Digital signature saved successfully"
},
"certificate-name": {
"label": "Certificate Name",
"placeholder": "Certificate Name"
},
"contact-info": {
"label": "Contact Information",
"placeholder": "Contact Information"
},
"location": {
"label": "Location",
"placeholder": "Location"
"remove": "Remove",
"save": "Save Changes",
"save-error": "Failed to save digital signature!",
"save-success": "Digital Signature Certificate successfully saved!"
},
"no-data": {
"action": "Upload Certificate",
"title": "No Digital Signature certificate is configured. For signing redacted documents please upload a PCKS.12 certificate."
},
"password": {
"label": "Certificate Password/Key",
"placeholder": "Password"
},
"reason": {
"label": "Reason",
"placeholder": "Reason"
"action": "Configure Certificate",
"title": "No Digital Signature Certificate.<br/>For signing redacted documents please configure a certificate."
}
},
"document-info": {

@ -1 +1 @@
Subproject commit a814fc8aa7a16c9acdaa9b7dd749e4493a54e1c2
Subproject commit e5542086ddd2e1548276d47d36161c89179d64e4

View File

@ -25,3 +25,4 @@ export * from './lib/trash';
export * from './lib/text-highlight';
export * from './lib/permissions';
export * from './lib/license';
export * from './lib/digital-signature';

View File

@ -0,0 +1,6 @@
export const DigitalSignatureOptions = {
KMS: 'KMS',
PKCS: 'PKCS',
} as const;
export type DigitalSignatureOption = keyof typeof DigitalSignatureOptions;

View File

@ -0,0 +1 @@
export * from './digital-signature-options';

View File

@ -1,6 +1,3 @@
export interface IDigitalSignatureRequest {
certificateName?: string;
contactInfo?: string;
location?: string;
reason?: string;
}

View File

@ -1,6 +0,0 @@
import { IDigitalSignatureRequest } from './digital-signature.request';
export interface IDigitalSignature extends IDigitalSignatureRequest {
base64EncodedPrivateKey?: string;
password?: string;
}

View File

@ -1,2 +1,5 @@
export * from './digital-signature.request';
export * from './digital-signature';
export * from './pkcs-digital-signature.request';
export * from './kms-digital-signature.request';
export * from './pkcs-digital-signature';
export * from './kms-digital-signature';
export * from './digital-signature-request';

View File

@ -0,0 +1,8 @@
import { IDigitalSignatureRequest } from './digital-signature-request';
export interface IKmsDigitalSignatureRequest extends IDigitalSignatureRequest {
kmsAccessKey?: string;
kmsKeyId?: string;
kmsRegion?: string;
kmsServiceEndpoint?: string;
}

View File

@ -0,0 +1,6 @@
import { IKmsDigitalSignatureRequest } from './kms-digital-signature.request';
export interface IKmsDigitalSignature extends IKmsDigitalSignatureRequest {
certificate?: string;
kmsSecretKey?: string;
}

View File

@ -0,0 +1,7 @@
import { IDigitalSignatureRequest } from './digital-signature-request';
export interface IPkcsDigitalSignatureRequest extends IDigitalSignatureRequest {
contactInfo?: string;
location?: string;
reason?: string;
}

View File

@ -0,0 +1,6 @@
import { IPkcsDigitalSignatureRequest } from './pkcs-digital-signature.request';
export interface IPkcsDigitalSignature extends IPkcsDigitalSignatureRequest {
base64EncodedPrivateKey?: string;
password?: string;
}

File diff suppressed because it is too large Load Diff