diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index cd31df82a..569e9506f 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.html new file mode 100644 index 000000000..f5eb8fb35 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.html @@ -0,0 +1,39 @@ +
+
+ +
+
+ +
+ + {{ 'digital-signature-dialog.upload-warning-message' | translate }} + + + + +
+ +
+ + +
+
+ + +
+
+
+ + +
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.scss new file mode 100644 index 000000000..9348c9978 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.scss @@ -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); + } + } +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.ts new file mode 100644 index 000000000..b81cb2779 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component.ts @@ -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[] = [ + { + 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, + 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 { + 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')); + } + } + } +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/base-signature-configuration-component.ts b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/base-signature-configuration-component.ts new file mode 100644 index 000000000..d25bca73c --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/base-signature-configuration-component.ts @@ -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 { + const observable: Observable = + this._selectedOption === DigitalSignatureOptions.PKCS + ? this._digitalSignatureService.deleteSignature() + : this._digitalSignatureService.deleteKmsSignature(); + return firstValueFrom(observable); + } + + abstract addRemoveCertificate(file: File | null): void; + + abstract save(): Promise; +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.html b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.html new file mode 100644 index 000000000..f505c99b5 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.html @@ -0,0 +1,39 @@ + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.scss new file mode 100644 index 000000000..a52b187c3 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.scss @@ -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; +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.ts new file mode 100644 index 000000000..0f1810b23 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component.ts @@ -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 = fileReader.result; + this.form.get('certificate').setValue(binaryContent); + }; + fileReader.readAsBinaryString(file as Blob); + } else { + this.form.controls['certificate'].setValue(''); + } + } + + save(): Promise { + const formValue = this.form.getRawValue(); + const digitalSignature: IKmsDigitalSignature = { ...formValue }; + + if (!this.digitalSignature) { + digitalSignature.certificate = window.btoa(digitalSignature.certificate); + } + + return firstValueFrom(this._digitalSignatureService.saveKmsSignature(digitalSignature)); + } +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.html b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.html new file mode 100644 index 000000000..2e635dc5a --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.html @@ -0,0 +1,33 @@ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.scss b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.scss new file mode 100644 index 000000000..170cba86c --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.scss @@ -0,0 +1,13 @@ +textarea { + resize: none; +} + +.w-400 { + width: 400px; +} + +iqser-upload-file { + display: block; + margin-top: 24px; + margin-bottom: 24px; +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.ts new file mode 100644 index 000000000..ed5afee29 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component.ts @@ -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 = 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 { + const formValue = this.form.getRawValue(); + const digitalSignature: IPkcsDigitalSignature = { ...formValue }; + + const observable = this.digitalSignature + ? this._digitalSignatureService.updateSignature(digitalSignature) + : this._digitalSignatureService.saveSignature(digitalSignature); + return firstValueFrom(observable); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html index 6c9083faf..c54eeefa0 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.html @@ -13,91 +13,42 @@
-
- + - + + + + -
- - -
+
+
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - - -
-
- + (click)="removeDigitalSignature()" + *ngIf="digitalSignature" + class="all-caps-label cancel" + translate="digital-signature-screen.action.remove" + >
+
diff --git a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts index ba3adff2c..c1edad42e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/digital-signature/digital-signature-screen.component.ts @@ -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 { - await this.loadDigitalSignatureAndInitializeForm(); - } - - async saveDigitalSignature(): Promise { - 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 { 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 = 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 { - this._loadingService.start(); + async saveDigitalSignature(): Promise { 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 { + this._loadingService.start(); + this.digitalSignature = await firstValueFrom(this._digitalSignatureService.getSignature()); + this._loadingService.stop(); + this._changeDetectorRef.detectChanges(); + } } diff --git a/apps/red-ui/src/app/modules/admin/services/admin-dialog.service.ts b/apps/red-ui/src/app/modules/admin/services/admin-dialog.service.ts index c65798eb2..675b2f604 100644 --- a/apps/red-ui/src/app/modules/admin/services/admin-dialog.service.ts +++ b/apps/red-ui/src/app/modules/admin/services/admin-dialog.service.ts @@ -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 { @@ -95,6 +97,10 @@ export class AdminDialogService extends DialogService { deleteDossierState: { component: ConfirmDeleteDossierStateDialogComponent, }, + configureCertificate: { + component: ConfigureCertificateDialogComponent, + dialogConfig: { disableClose: false, maxHeight: '100vh' }, + }, }; constructor( diff --git a/apps/red-ui/src/app/modules/admin/services/digital-signature.service.ts b/apps/red-ui/src/app/modules/admin/services/digital-signature.service.ts index 005bb8b08..6024e3322 100644 --- a/apps/red-ui/src/app/modules/admin/services/digital-signature.service.ts +++ b/apps/red-ui/src/app/modules/admin/services/digital-signature.service.ts @@ -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 { @@ -10,20 +17,34 @@ export class DigitalSignatureService extends GenericService { + updateSignature(@RequiredParam() body: IPkcsDigitalSignatureRequest): Observable { return this._put(body); } @Validate() - save(@RequiredParam() body: IDigitalSignature): Observable { + saveSignature(@RequiredParam() body: IPkcsDigitalSignature): Observable { return this._post(body); } - delete(): Observable { + @Validate() + saveKmsSignature(@RequiredParam() body: IKmsDigitalSignature): Observable { + return this._post(body, `${this._defaultModelPath}/kms`); + } + + deleteSignature(): Observable { return super.delete({}); } + deleteKmsSignature(): Observable { + return super.delete({}, `${this._defaultModelPath}/kms`); + } + getSignature(): Observable { - 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), + ); } } diff --git a/apps/red-ui/src/app/modules/admin/translations/digital-signature-dialog-translations.ts b/apps/red-ui/src/app/modules/admin/translations/digital-signature-dialog-translations.ts new file mode 100644 index 000000000..b4b99925c --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/translations/digital-signature-dialog-translations.ts @@ -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; diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.html b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.html index 5c032b36c..21cb3dcdf 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.html +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.html @@ -3,37 +3,21 @@
-
- -
-
- -
- -

{{ fileToImport.name }}

- -
-
- - {{ 'import-redactions-dialog.only-for-specific-pages' | translate }} - + +
+ + {{ 'import-redactions-dialog.only-for-specific-pages' | translate }} + -
- -
+
+
- +
@@ -45,5 +29,3 @@ - - diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.scss b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.scss index 7fca79465..0f476c1e4 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.scss +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.scss @@ -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; diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.ts index 52f036362..a2807682b 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/import-redactions-dialog/import-redactions-dialog.ts @@ -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 { this._loadingService.start(); const import$ = this._redactionImportService.importRedactions(this.data.dossierId, this.data.fileId, this.fileToImport); diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss index cadbd6f7a..029a1182d 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.scss @@ -24,14 +24,6 @@ padding: 0; } -.fields-container { - flex-direction: column; - - &:first-child { - margin-right: 40px; - } -} - redaction-small-chip { margin-right: 8px; } diff --git a/apps/red-ui/src/app/modules/shared/directives/drag-drop-file-upload.directive.ts b/apps/red-ui/src/app/modules/shared/directives/drag-drop-file-upload.directive.ts deleted file mode 100644 index 77daf3780..000000000 --- a/apps/red-ui/src/app/modules/shared/directives/drag-drop-file-upload.directive.ts +++ /dev/null @@ -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(); - @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 } }); - } - } - } -} diff --git a/apps/red-ui/src/app/modules/shared/shared.module.ts b/apps/red-ui/src/app/modules/shared/shared.module.ts index ed05b6c45..4dbf5004b 100644 --- a/apps/red-ui/src/app/modules/shared/shared.module.ts +++ b/apps/red-ui/src/app/modules/shared/shared.module.ts @@ -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]; diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index cd3f49f26..973f53c6d 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -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": { diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 5060f4f45..38839e57d 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -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.
For signing redacted documents please configure a certificate." } }, "document-info": { diff --git a/libs/common-ui b/libs/common-ui index a814fc8aa..e5542086d 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit a814fc8aa7a16c9acdaa9b7dd749e4493a54e1c2 +Subproject commit e5542086ddd2e1548276d47d36161c89179d64e4 diff --git a/libs/red-domain/src/index.ts b/libs/red-domain/src/index.ts index 31a7e92e8..92d56faad 100644 --- a/libs/red-domain/src/index.ts +++ b/libs/red-domain/src/index.ts @@ -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'; diff --git a/libs/red-domain/src/lib/digital-signature/digital-signature-options.ts b/libs/red-domain/src/lib/digital-signature/digital-signature-options.ts new file mode 100644 index 000000000..c012fec01 --- /dev/null +++ b/libs/red-domain/src/lib/digital-signature/digital-signature-options.ts @@ -0,0 +1,6 @@ +export const DigitalSignatureOptions = { + KMS: 'KMS', + PKCS: 'PKCS', +} as const; + +export type DigitalSignatureOption = keyof typeof DigitalSignatureOptions; diff --git a/libs/red-domain/src/lib/digital-signature/index.ts b/libs/red-domain/src/lib/digital-signature/index.ts new file mode 100644 index 000000000..2502c7af8 --- /dev/null +++ b/libs/red-domain/src/lib/digital-signature/index.ts @@ -0,0 +1 @@ +export * from './digital-signature-options'; diff --git a/libs/red-domain/src/lib/signature/digital-signature.request.ts b/libs/red-domain/src/lib/signature/digital-signature-request.ts similarity index 52% rename from libs/red-domain/src/lib/signature/digital-signature.request.ts rename to libs/red-domain/src/lib/signature/digital-signature-request.ts index d7d9fa618..3eb55d4ac 100644 --- a/libs/red-domain/src/lib/signature/digital-signature.request.ts +++ b/libs/red-domain/src/lib/signature/digital-signature-request.ts @@ -1,6 +1,3 @@ export interface IDigitalSignatureRequest { certificateName?: string; - contactInfo?: string; - location?: string; - reason?: string; } diff --git a/libs/red-domain/src/lib/signature/digital-signature.ts b/libs/red-domain/src/lib/signature/digital-signature.ts deleted file mode 100644 index 0b971ca70..000000000 --- a/libs/red-domain/src/lib/signature/digital-signature.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IDigitalSignatureRequest } from './digital-signature.request'; - -export interface IDigitalSignature extends IDigitalSignatureRequest { - base64EncodedPrivateKey?: string; - password?: string; -} diff --git a/libs/red-domain/src/lib/signature/index.ts b/libs/red-domain/src/lib/signature/index.ts index f5a313484..a6c32906c 100644 --- a/libs/red-domain/src/lib/signature/index.ts +++ b/libs/red-domain/src/lib/signature/index.ts @@ -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'; diff --git a/libs/red-domain/src/lib/signature/kms-digital-signature.request.ts b/libs/red-domain/src/lib/signature/kms-digital-signature.request.ts new file mode 100644 index 000000000..f8261ba11 --- /dev/null +++ b/libs/red-domain/src/lib/signature/kms-digital-signature.request.ts @@ -0,0 +1,8 @@ +import { IDigitalSignatureRequest } from './digital-signature-request'; + +export interface IKmsDigitalSignatureRequest extends IDigitalSignatureRequest { + kmsAccessKey?: string; + kmsKeyId?: string; + kmsRegion?: string; + kmsServiceEndpoint?: string; +} diff --git a/libs/red-domain/src/lib/signature/kms-digital-signature.ts b/libs/red-domain/src/lib/signature/kms-digital-signature.ts new file mode 100644 index 000000000..db37bea71 --- /dev/null +++ b/libs/red-domain/src/lib/signature/kms-digital-signature.ts @@ -0,0 +1,6 @@ +import { IKmsDigitalSignatureRequest } from './kms-digital-signature.request'; + +export interface IKmsDigitalSignature extends IKmsDigitalSignatureRequest { + certificate?: string; + kmsSecretKey?: string; +} diff --git a/libs/red-domain/src/lib/signature/pkcs-digital-signature.request.ts b/libs/red-domain/src/lib/signature/pkcs-digital-signature.request.ts new file mode 100644 index 000000000..06eb9bf84 --- /dev/null +++ b/libs/red-domain/src/lib/signature/pkcs-digital-signature.request.ts @@ -0,0 +1,7 @@ +import { IDigitalSignatureRequest } from './digital-signature-request'; + +export interface IPkcsDigitalSignatureRequest extends IDigitalSignatureRequest { + contactInfo?: string; + location?: string; + reason?: string; +} diff --git a/libs/red-domain/src/lib/signature/pkcs-digital-signature.ts b/libs/red-domain/src/lib/signature/pkcs-digital-signature.ts new file mode 100644 index 000000000..16a6ebfad --- /dev/null +++ b/libs/red-domain/src/lib/signature/pkcs-digital-signature.ts @@ -0,0 +1,6 @@ +import { IPkcsDigitalSignatureRequest } from './pkcs-digital-signature.request'; + +export interface IPkcsDigitalSignature extends IPkcsDigitalSignatureRequest { + base64EncodedPrivateKey?: string; + password?: string; +} diff --git a/paligo-theme/paligo-styles/redacto-theme.css b/paligo-theme/paligo-styles/redacto-theme.css index 7356127bf..8489de284 100644 --- a/paligo-theme/paligo-styles/redacto-theme.css +++ b/paligo-theme/paligo-styles/redacto-theme.css @@ -1,492 +1,492 @@ @charset "UTF-8"; -@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); .portal-header { - z-index: 1; - height: 450px; + z-index: 1; + height: 450px; } .portal-header::before { - background-color: #283241; + background-color: #283241; } .portal-header h1 { - font-size: 64px; - font-weight: 300; - line-height: 87px; - margin-top: 68px; - margin-bottom: 40px; + font-size: 64px; + font-weight: 300; + line-height: 87px; + margin-top: 68px; + margin-bottom: 40px; } .portal-header .portal-search { - max-width: 600px; - margin: auto; - position: relative; + max-width: 600px; + margin: auto; + position: relative; } .portal-header .portal-search .search-field { - width: 100%; - border: 1px solid #d3d5da; - border-radius: 8px; - background-color: #fff; + width: 100%; + border: 1px solid #d3d5da; + border-radius: 8px; + background-color: #fff; } .portal-header .portal-search .search-field::placeholder { - opacity: 0.7; + opacity: 0.7; } .portal-header .portal-search .search-field, .portal-header .portal-search .search-field::placeholder { - color: #283241; - font-size: 14px; - line-height: 18px; + color: #283241; + font-size: 14px; + line-height: 18px; } .portal-header .portal-search .search-field { - padding: 12px 17px; + padding: 12px 17px; } .portal-header .portal-search .btn { - position: absolute; - right: 0; - padding: 11px 18px; - background-color: transparent; - color: #283241; - cursor: pointer; - border-radius: 0 8px 8px 0; + position: absolute; + right: 0; + padding: 11px 18px; + background-color: transparent; + color: #283241; + cursor: pointer; + border-radius: 0 8px 8px 0; } .portal-header .portal-search .btn:hover { - background-color: #dd4d50; + background-color: #dd4d50; } @media only screen and (max-width: 768px) { - .portal-header h1 { - font-size: 42px; - font-weight: 300; - line-height: 57px; - } + .portal-header h1 { + font-size: 42px; + font-weight: 300; + line-height: 57px; + } } .portal-single-publication { - background-color: transparent; - width: 280px; + background-color: transparent; + width: 280px; } .portal-single-publication > a { - border-radius: 4px; + border-radius: 4px; } .portal-single-publication .publication-icon { - background-color: #dd4d50; + background-color: #dd4d50; } .featured-content-label { - margin-top: 24px; - text-align: center; + margin-top: 24px; + text-align: center; } .featured-content { - margin-top: 24px; - margin-bottom: 0; + margin-top: 24px; + margin-bottom: 0; } .featured-content .inner { - margin: 0; - justify-content: center; + margin: 0; + justify-content: center; } .featured-content .publication-contents { - padding: 24px 40px; - border: 1px solid #e2e4e9; - width: 100%; - margin: 0; - background-color: #fff; - border-radius: 4px; - border: none; - background-color: transparent; - width: 250px; - margin: 0 15px !important; + padding: 24px 40px; + border: 1px solid #e2e4e9; + width: 100%; + margin: 0; + background-color: #fff; + border-radius: 4px; + border: none; + background-color: transparent; + width: 250px; + margin: 0 15px !important; } .featured-content .publication-contents h4.featured-title, .featured-content .publication-contents .section-toc-title { - margin: 0; + margin: 0; } .featured-content .publication-contents h4.featured-title a, .featured-content .publication-contents .section-toc-title a { - color: #283241; + color: #283241; } .featured-content .publication-contents h4.featured-title a:hover, .featured-content .publication-contents .section-toc-title a:hover { - color: #283241; - text-decoration: underline; + color: #283241; + text-decoration: underline; } .featured-content .publication-contents .section-toc-title { - font-size: 28px; - font-weight: 300; - line-height: 36px; + font-size: 28px; + font-weight: 300; + line-height: 36px; } .featured-content .publication-contents ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .featured-content .publication-contents li { - margin: 4px 0; + margin: 4px 0; } .featured-content .publication-contents li:first-child { - margin-top: 20px; + margin-top: 20px; } .featured-content .publication-contents li:last-child { - margin-bottom: 40px; + margin-bottom: 40px; } .featured-content .publication-contents li a { - color: #dd4d50; - font-size: 16px; - line-height: 24px; + color: #dd4d50; + font-size: 16px; + line-height: 24px; } .featured-content .publication-contents li a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .featured-content .publication-contents h4 span, .featured-content .publication-contents li::before { - display: none; + display: none; } .featured-content .publication-contents li a { - font-size: inherit; - line-height: inherit; + font-size: inherit; + line-height: inherit; } /* Einleitung */ .cat-panel-1:before { - content: "\f007"; + content: '\f007'; } /* Workflow */ .cat-panel-2:before { - content: "\f085"; + content: '\f085'; } /* Voraussetzungen */ .cat-panel-3:before { - content: "\f109"; + content: '\f109'; } /* Benutzermenü und -profil */ .cat-panel-4:before { - content: "\f007"; + content: '\f007'; } /* Benachrichtigungen */ .cat-panel-5:before { - content: "\f0f3"; + content: '\f0f3'; } /* Suchfunktion */ .cat-panel-6:before { - content: "\f002"; + content: '\f002'; } /* Ebenen in der Benutzeroberfläche des RedactManagers */ .cat-panel-7:before { - content: "\f248"; + content: '\f248'; } /* Rollen und Berechtigungen */ .cat-panel-8:before { - content: "\f084"; + content: '\f084'; } /* Dossier erstellen und verwalten */ .cat-panel-9:before { - content: "\f07c"; + content: '\f07c'; } /* Dokumente bearbeiten im Editor */ .cat-panel-10:before { - content: "\f15c"; + content: '\f15c'; } /* Dossier abschließen und herunterladen */ .cat-panel-11:before { - content: "\f019"; + content: '\f019'; } /* Funktionsübersicht */ .cat-panel-12:before { - content: "\f03a"; + content: '\f03a'; } /* Glossar */ .cat-panel-13:before { - content: "\f02d"; + content: '\f02d'; } /* FAQ’s (häufige Fragen) */ .cat-panel-14:before { - content: "\f059"; + content: '\f059'; } .portal-search-result { - background-color: #f5f5f7; + background-color: #f5f5f7; } .search-container { - padding-bottom: 100px; + padding-bottom: 100px; } .portal-search-result { - padding: 80px 0 0 0; + padding: 80px 0 0 0; } ul.searchresults { - border: 1px solid #e2e4e9; - background-color: #fff; - border-radius: 4px; - margin-top: 32px; + border: 1px solid #e2e4e9; + background-color: #fff; + border-radius: 4px; + margin-top: 32px; } ul.searchresults .search-highlight { - font-style: normal; + font-style: normal; } li.searchresultitem { - margin: 0 32px; - border-bottom: 1px solid #e2e4e9; - padding: 32px 8px; + margin: 0 32px; + border-bottom: 1px solid #e2e4e9; + padding: 32px 8px; } .searchresultitem.selected-searchresultitem { - background-color: transparent; - border-radius: 0; + background-color: transparent; + border-radius: 0; } .searchresulttitle { - font-size: 28px; - font-weight: 300; - line-height: 36px; - color: #283241; + font-size: 28px; + font-weight: 300; + line-height: 36px; + color: #283241; } .searchresultsnippet { - margin: 16px 0; - color: #283241; + margin: 16px 0; + color: #283241; } .search-result-breadcrumbs { - color: #dd4d50; + color: #dd4d50; } .portal-footer, .site-footer { - border-top: 1px solid #e2e4e9; - padding: 0; + border-top: 1px solid #e2e4e9; + padding: 0; } .portal-footer.portal-footer, .site-footer.portal-footer { - margin-top: 100px; + margin-top: 100px; } .portal-footer .inner, .site-footer .inner { - margin: 0; - padding: 8px 0 64px 0; - font-size: 16px; - line-height: 24px; + margin: 0; + padding: 8px 0 64px 0; + font-size: 16px; + line-height: 24px; } .portal-footer .inner > *, .site-footer .inner > * { - padding: 0; + padding: 0; } .portal-footer .inner .copyright, .site-footer .inner .copyright { - width: 50%; + width: 50%; } :root { - --iqser-primary: lightblue; - --iqser-primary-rgb: 220, 230, 234; - --iqser-primary-2: orange; - --iqser-accent: blue; - --iqser-accent-rgb: 123, 234, 111; - --iqser-disabled: #9398a0; - --iqser-not-disabled-table-item: #f9fafb; - --iqser-btn-bg-hover: #e2e4e9; - --iqser-btn-bg: #f0f1f4; - --iqser-warn: #fdbd00; - --iqser-white: white; - --iqser-black: black; - --iqser-light: white; - --iqser-separator: rgba(226, 228, 233, 0.9); - --iqser-quick-filter-border: #d3d5da; - --iqser-grey-1: #283241; - --iqser-grey-2: #f4f5f7; - --iqser-grey-3: #aaacb3; - --iqser-grey-4: #e2e4e9; - --iqser-grey-5: #d3d5da; - --iqser-grey-6: #f0f1f4; - --iqser-grey-7: #9398a0; - --iqser-grey-8: #f9fafb; - --iqser-grey-9: #f5f5f7; - --iqser-grey-10: #313d4e; - --iqser-grey-11: #ecedf0; - --iqser-green-1: #00ff00; - --iqser-green-2: #5ce594; - --iqser-orange-1: #ff801a; - --iqser-yellow-1: #ffb83b; - --iqser-yellow-2: #fdbd00; - --iqser-yellow-rgb: 253, 189, 0; - --iqser-red-1: #dd4d50; - --iqser-red-2: #f16164; - --iqser-blue-1: #4875f7; - --iqser-blue-2: #48c9f7; - --iqser-blue-3: #5b97db; - --iqser-blue-4: #374c81; - --iqser-blue-5: #c5d3eb; - --iqser-pink-1: #f125de; - --iqser-helpmode-primary: green; + --iqser-primary: lightblue; + --iqser-primary-rgb: 220, 230, 234; + --iqser-primary-2: orange; + --iqser-accent: blue; + --iqser-accent-rgb: 123, 234, 111; + --iqser-disabled: #9398a0; + --iqser-not-disabled-table-item: #f9fafb; + --iqser-btn-bg-hover: #e2e4e9; + --iqser-btn-bg: #f0f1f4; + --iqser-warn: #fdbd00; + --iqser-white: white; + --iqser-black: black; + --iqser-light: white; + --iqser-separator: rgba(226, 228, 233, 0.9); + --iqser-quick-filter-border: #d3d5da; + --iqser-grey-1: #283241; + --iqser-grey-2: #f4f5f7; + --iqser-grey-3: #aaacb3; + --iqser-grey-4: #e2e4e9; + --iqser-grey-5: #d3d5da; + --iqser-grey-6: #f0f1f4; + --iqser-grey-7: #9398a0; + --iqser-grey-8: #f9fafb; + --iqser-grey-9: #f5f5f7; + --iqser-grey-10: #313d4e; + --iqser-grey-11: #ecedf0; + --iqser-green-1: #00ff00; + --iqser-green-2: #5ce594; + --iqser-orange-1: #ff801a; + --iqser-yellow-1: #ffb83b; + --iqser-yellow-2: #fdbd00; + --iqser-yellow-rgb: 253, 189, 0; + --iqser-red-1: #dd4d50; + --iqser-red-2: #f16164; + --iqser-blue-1: #4875f7; + --iqser-blue-2: #48c9f7; + --iqser-blue-3: #5b97db; + --iqser-blue-4: #374c81; + --iqser-blue-5: #c5d3eb; + --iqser-pink-1: #f125de; + --iqser-helpmode-primary: green; } .site-sidebar { - background-color: #283241; - scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); - scrollbar-width: thin; - /* Track */ - /* Handle */ + background-color: #283241; + scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); + scrollbar-width: thin; + /* Track */ + /* Handle */ } .site-sidebar .logo { - padding: 24px 0 30px 0 !important; + padding: 24px 0 30px 0 !important; } .site-sidebar::-webkit-scrollbar { - width: 11px; + width: 11px; } .site-sidebar::-webkit-scrollbar-track { - background: var(--iqser-grey-2); + background: var(--iqser-grey-2); } .site-sidebar::-webkit-scrollbar-thumb { - background: var(--iqser-quick-filter-border); + background: var(--iqser-quick-filter-border); } .site-sidebar-search { - padding: 0 24px; + padding: 0 24px; } .site-sidebar-search .search-field { - width: 100%; - border: 1px solid #d3d5da; - border-radius: 8px; - background-color: #fff; + width: 100%; + border: 1px solid #d3d5da; + border-radius: 8px; + background-color: #fff; } .site-sidebar-search .search-field::placeholder { - opacity: 0.7; + opacity: 0.7; } .site-sidebar-search .search-field, .site-sidebar-search .search-field::placeholder { - color: #283241; - font-size: 14px; - line-height: 18px; + color: #283241; + font-size: 14px; + line-height: 18px; } .site-sidebar-search .search-field { - padding: 7px 13px; + padding: 7px 13px; } .nav-site-sidebar { - margin-top: 16px; + margin-top: 16px; } .nav-site-sidebar .topic-link { - padding-top: 11px; - padding-bottom: 11px; - font-size: 14px; - line-height: 18px; - color: #d3d5da; + padding-top: 11px; + padding-bottom: 11px; + font-size: 14px; + line-height: 18px; + color: #d3d5da; } .nav-site-sidebar .topic-link:hover { - background-color: #313d4e; + background-color: #313d4e; } .nav-site-sidebar .active > .topic-link { - background-color: #313d4e; + background-color: #313d4e; } .nav-site-sidebar .active > a { - color: #fff; - font-weight: 600; + color: #fff; + font-weight: 600; } .nav-site-sidebar > li > a { - padding-left: 24px; + padding-left: 24px; } .nav-site-sidebar > li > ul > li > a { - padding-left: 32px; + padding-left: 32px; } .nav-site-sidebar > li > ul > li > ul > li > a { - padding-left: 40px; + padding-left: 40px; } .toc .glyphicon { - top: 5px; + top: 5px; } .toc > li > .topic-link > .glyphicon { - margin-top: 4px; + margin-top: 4px; } .toolbar { - box-shadow: none; - padding: 21px 24px; - margin-bottom: 50px; + box-shadow: none; + padding: 21px 24px; + margin-bottom: 50px; } .topic-content .breadcrumb-container { - margin-top: 40px; + margin-top: 40px; } .topic-content .breadcrumb { - font-size: 14px; - line-height: 18px; - font-weight: 600; + font-size: 14px; + line-height: 18px; + font-weight: 600; } .topic-content .breadcrumb a { - color: #283241; + color: #283241; } .topic-content .breadcrumb a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .topic-content .breadcrumb .breadcrumb-node { - color: #dd4d50; + color: #dd4d50; } main article { - margin-bottom: 0; - padding: 0; + margin-bottom: 0; + padding: 0; } section { - position: relative; + position: relative; } section > .titlepage .title { - margin: 24px 0 16px 0; + margin: 24px 0 16px 0; } #topic-content > section > .titlepage h2.title { - margin: 0 0 24px; + margin: 0 0 24px; } .image-viewport { - margin: auto; + margin: auto; } .image-viewport img { - margin: 16px auto; + margin: 16px auto; } .pager { - margin-top: 30px; - margin-bottom: 30px; - padding: 0; + margin-top: 30px; + margin-bottom: 30px; + padding: 0; } .pager li > a, .pager li > span { - color: #dd4d50; - font-size: 14px; - font-weight: 600; - line-height: 19px; - text-transform: uppercase; - padding: 0; - background-color: transparent; - border: none; - border-radius: 0; + color: #dd4d50; + font-size: 14px; + font-weight: 600; + line-height: 19px; + text-transform: uppercase; + padding: 0; + background-color: transparent; + border: none; + border-radius: 0; } .pager li > a:hover, .pager li > span:hover { - text-decoration: underline; - background-color: transparent; - color: #dd4d50; + text-decoration: underline; + background-color: transparent; + color: #dd4d50; } .checklist-reset-wrapper { - position: absolute; - right: 0; - top: 0; + position: absolute; + right: 0; + top: 0; } .warning, @@ -494,229 +494,240 @@ section > .titlepage .title { .important, .caution, .tip { - margin-top: 32px; - margin-bottom: 32px; - padding: 16px 24px 16px 68px; - background-color: #fff; - border-left: 4px solid #dd4d50; - border-radius: 4px; + margin-top: 32px; + margin-bottom: 32px; + padding: 16px 24px 16px 68px; + background-color: #fff; + border-left: 4px solid #dd4d50; + border-radius: 4px; } .warning:before, .note:before, .important:before, .caution:before, .tip:before { - color: #dd4d50; - width: 20px; - height: 20px; - text-align: center; - left: 24px; - top: calc(50% - 15px); + color: #dd4d50; + width: 20px; + height: 20px; + text-align: center; + left: 24px; + top: calc(50% - 15px); } .warning h3, .note h3, .important h3, .caution h3, .tip h3 { - padding: 0; - font-size: 18px; - font-weight: 600; - line-height: 24px; - margin-bottom: 8px; + padding: 0; + font-size: 18px; + font-weight: 600; + line-height: 24px; + margin-bottom: 8px; } .warning p, .note p, .important p, .caution p, .tip p { - line-height: 20px; + line-height: 20px; } .topic-content > section > p { - margin: 12px 0; + margin: 12px 0; } .panel { - padding: 12px 0; - border-radius: 4px; - border: none; + padding: 12px 0; + border-radius: 4px; + border: none; } .panel .panel-body > p { - margin-bottom: 12px; + margin-bottom: 12px; } .panel .panel-body > p:not(:first-of-type) { - margin-top: 18px; + margin-top: 18px; } .mediaobject { - margin-top: 20px; + margin-top: 20px; } .mediaobject img { - border-radius: 4px; - margin: 0; - box-shadow: 0 3px 12px 5px rgba(40, 50, 65, 0.14); + border-radius: 4px; + margin: 0; + box-shadow: 0 3px 12px 5px rgba(40, 50, 65, 0.14); } .mediaobject .caption > p { - font-size: 14px; - text-align: center; - font-style: italic; - margin: 0; + font-size: 14px; + text-align: center; + font-style: italic; + margin: 0; } .inlinemediaobject { - vertical-align: unset; + vertical-align: unset; } main ol, main ul { - margin: 0 0 24px; + margin: 0 0 24px; } .section-toc { - padding: 24px 40px; - border: 1px solid #e2e4e9; - width: 100%; - margin: 0; - background-color: #fff; - border-radius: 4px; + padding: 24px 40px; + border: 1px solid #e2e4e9; + width: 100%; + margin: 0; + background-color: #fff; + border-radius: 4px; } .section-toc h4.featured-title, .section-toc .section-toc-title { - margin: 0; + margin: 0; } .section-toc h4.featured-title a, .section-toc .section-toc-title a { - color: #283241; + color: #283241; } .section-toc h4.featured-title a:hover, .section-toc .section-toc-title a:hover { - color: #283241; - text-decoration: underline; + color: #283241; + text-decoration: underline; } .section-toc .section-toc-title { - font-size: 28px; - font-weight: 300; - line-height: 36px; + font-size: 28px; + font-weight: 300; + line-height: 36px; } .section-toc ul { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .section-toc li { - margin: 4px 0; + margin: 4px 0; } .section-toc li:first-child { - margin-top: 20px; + margin-top: 20px; } .section-toc li:last-child { - margin-bottom: 40px; + margin-bottom: 40px; } .section-toc li a { - color: #dd4d50; - font-size: 16px; - line-height: 24px; + color: #dd4d50; + font-size: 16px; + line-height: 24px; } .section-toc li a:hover { - color: #dd4d50; - text-decoration: underline; + color: #dd4d50; + text-decoration: underline; } .section-toc h4 span, .section-toc li::before { - display: none; + display: none; } .section-toc li:first-child { - margin-top: 16px; + margin-top: 16px; } .section-toc li:last-child { - margin-bottom: 8px; + margin-bottom: 8px; } .procedure > li.step::before { - background-color: transparent; - border: 1px solid #dd4d50; - color: #dd4d50; - line-height: 23px; + background-color: transparent; + border: 1px solid #dd4d50; + color: #dd4d50; + line-height: 23px; } .question { - font-weight: 600; + font-weight: 600; } .question > td > p { - margin: 32px 0 18px 0; + margin: 32px 0 18px 0; } .question > td:first-child { - padding-right: 4px; + padding-right: 4px; } .fixed-toolbar article.topic :target.question:before { - content: none; + content: none; } body { - color: #283241; - background-color: #f5f5f7; - font-family: "Open Sans", sans-serif; - scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); - scrollbar-width: thin; - /* Track */ - /* Handle */ + color: #283241; + background-color: #f5f5f7; + font-family: 'Open Sans', sans-serif; + scrollbar-color: var(--iqser-quick-filter-border) var(--iqser-grey-2); + scrollbar-width: thin; + /* Track */ + /* Handle */ } -body h1, body .h1, -body h2, body .h2, -body h3, body .h3, -body h4, body .h4, -body h5, body .h5, -body h6, body .h6, +body h1, +body .h1, +body h2, +body .h2, +body h3, +body .h3, +body h4, +body .h4, +body h5, +body .h5, +body h6, +body .h6, body p, body pre { - margin: 0; - font-family: "Open Sans", sans-serif; + margin: 0; + font-family: 'Open Sans', sans-serif; } body::-webkit-scrollbar { - width: 11px; + width: 11px; } body::-webkit-scrollbar-track { - background: var(--iqser-grey-2); + background: var(--iqser-grey-2); } body::-webkit-scrollbar-thumb { - background: var(--iqser-quick-filter-border); + background: var(--iqser-quick-filter-border); } -body h1, body .h1 { - font-size: 64px; - font-weight: 300; - line-height: 87px; +body h1, +body .h1 { + font-size: 64px; + font-weight: 300; + line-height: 87px; } -body h2, body .h2 { - font-size: 42px; - font-weight: 300; - line-height: 57px; +body h2, +body .h2 { + font-size: 42px; + font-weight: 300; + line-height: 57px; } -body h3, body .h3 { - font-size: 32px; - font-weight: 300; - line-height: 43px; +body h3, +body .h3 { + font-size: 32px; + font-weight: 300; + line-height: 43px; } -body h4, body .h4 { - font-size: 28px; - font-weight: 300; - line-height: 36px; +body h4, +body .h4 { + font-size: 28px; + font-weight: 300; + line-height: 36px; } -body h5, body .h5 { - font-size: 18px; - font-weight: 600; - line-height: 24px; +body h5, +body .h5 { + font-size: 18px; + font-weight: 600; + line-height: 24px; } body p { - font-size: 16px; - line-height: 24px; + font-size: 16px; + line-height: 24px; } body a { - color: #dd4d50; + color: #dd4d50; } body a:hover { - text-decoration: underline; - color: #dd4d50; + text-decoration: underline; + color: #dd4d50; } body a:focus { - color: #dd4d50; + color: #dd4d50; }