From 5cd51374d7e746c36da77314d7d84f8361de36a1 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 15 Jun 2023 21:03:48 -0500 Subject: [PATCH] [AC-1416] Expose Organization Fingerprint (#5557) * refactor: change getFingerprint param to fingerprintMaterial, refs PM-1522 * feat: generate and show fingerprint for organization (WIP), refs AC-1416 * feat: update legacy params subscription to best practice (WIP), refs AC-1461 * refactor: update to use reactive forms, refs AC-1416 * refactor: remove boostrap specific classes and update to component library paradigms, refs AC-1416 * refactor: remove boostrap specific classes and update to component library paradigms, refs AC-1416 * refactor: create shared fingerprint component to redude boilerplate for settings fingerprint views, refs AC-1416 * refactor: use grid to emulate col-6 and remove unnecessary theme extensions, refs AC-1416 * refactor: remove negative margin and clean up extra divs, refs AC-1416 * [AC-1431] Add missing UserVerificationModule import (#5555) * [PM-2238] Add nord and solarize themes (#5491) * Fix simple configurable dialog stories (#5560) * chore(deps): update bitwarden/gh-actions digest to 72594be (#5523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * refactor: remove extra div leftover from card-body class, refs AC-1416 * refactor: use bitTypography for headers, refs AC-1416 * fix: update crypto service abstraction path, refs AC-1416 * refactor: remove try/catch on handler, remove bootstrap class, update api chaining in observable, refs AC-1416 * fix: replace faulty combineLatest logic, refs AC-1416 * refactor: simplify observable logic again, refs AC-1416 --------- Co-authored-by: Shane Melton Co-authored-by: Oscar Hinton Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../settings/account.component.html | 103 +++++-------- .../settings/account.component.ts | 140 +++++++++++++----- .../settings/organization-settings.module.ts | 9 +- .../src/app/settings/profile.component.html | 18 +-- .../web/src/app/settings/profile.component.ts | 13 +- .../account-fingerprint.component.html | 16 ++ .../account-fingerprint.component.ts | 30 ++++ .../src/app/shared/loose-components.module.ts | 2 + apps/web/src/locales/en/messages.json | 4 + .../platform/abstractions/crypto.service.ts | 2 +- .../src/platform/services/crypto.service.ts | 4 +- 11 files changed, 206 insertions(+), 135 deletions(-) create mode 100644 apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.html create mode 100644 apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 08059ed99fd..c7ac9910ac5 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -1,95 +1,66 @@ - +

{{ "organizationInfo" | i18n }}

- {{ "loading" | i18n }} + {{ "loading" | i18n }}
-
-
-
-
- - -
-
- - -
-
- - -
+ +
+
+ + {{ "organizationName" | i18n }} + + + + {{ "billingEmail" | i18n }} + + + + {{ "businessName" | i18n }} + +
-
+
+ +
- -
-

{{ "apiKey" | i18n }}

-
+

{{ "apiKey" | i18n }}

{{ "apiKeyDesc" | i18n }} {{ "learnMore" | i18n }}

- -
-
-

{{ "dangerZone" | i18n }}

-
-
-
-

{{ "dangerZoneDesc" | i18n }}

- - -
+

{{ "dangerZone" | i18n }}

+
+

{{ "dangerZoneDesc" | i18n }}

+ +
diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 2d0c1b17bea..48dcb2c88c6 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -1,6 +1,7 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; +import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from } from "rxjs"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -13,6 +14,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ApiKeyComponent } from "../../../settings/api-key.component"; import { PurgeVaultComponent } from "../../../settings/purge-vault.component"; @@ -23,7 +25,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./ selector: "app-org-account", templateUrl: "account.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil export class AccountComponent { @ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef; @@ -40,7 +41,29 @@ export class AccountComponent { formPromise: Promise; taxFormPromise: Promise; - private organizationId: string; + // FormGroup validators taken from server Organization domain object + protected formGroup = this.formBuilder.group({ + orgName: this.formBuilder.control( + { value: "", disabled: true }, + { + validators: [Validators.required, Validators.maxLength(50)], + updateOn: "change", + } + ), + billingEmail: this.formBuilder.control( + { value: "", disabled: true }, + { validators: [Validators.required, Validators.email, Validators.maxLength(256)] } + ), + businessName: this.formBuilder.control( + { value: "", disabled: true }, + { validators: [Validators.maxLength(50)] } + ), + }); + + protected organizationId: string; + protected publicKeyBuffer: ArrayBuffer; + + private destroy$ = new Subject(); constructor( private modalService: ModalService, @@ -52,53 +75,88 @@ export class AccountComponent { private router: Router, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogServiceAbstraction + private dialogService: DialogServiceAbstraction, + private formBuilder: FormBuilder ) {} async ngOnInit() { this.selfHosted = this.platformUtilsService.isSelfHost(); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.parent.params.subscribe(async (params) => { - this.organizationId = params.organizationId; - this.canEditSubscription = this.organizationService.get( - this.organizationId - ).canEditSubscription; - try { - this.org = await this.organizationApiService.get(this.organizationId); - this.canUseApi = this.org.useApi; - } catch (e) { - this.logService.error(e); - } - }); - this.loading = false; + this.route.parent.parent.params + .pipe( + switchMap((params) => { + return combineLatest([ + // Organization domain + this.organizationService.get$(params.organizationId), + // OrganizationResponse for form population + from(this.organizationApiService.get(params.organizationId)), + // Organization Public Key + from(this.organizationApiService.getKeys(params.organizationId)), + ]); + }), + takeUntil(this.destroy$) + ) + .subscribe(([organization, orgResponse, orgKeys]) => { + // Set domain level organization variables + this.organizationId = organization.id; + this.canEditSubscription = organization.canEditSubscription; + this.canUseApi = organization.useApi; + + // Org Response + this.org = orgResponse; + + // Public Key Buffer for Org Fingerprint Generation + this.publicKeyBuffer = Utils.fromB64ToArray(orgKeys?.publicKey)?.buffer; + + // Patch existing values + this.formGroup.patchValue({ + orgName: this.org.name, + billingEmail: this.org.billingEmail, + businessName: this.org.businessName, + }); + + // Update disabled states - reactive forms prefers not using disabled attribute + if (!this.selfHosted) { + this.formGroup.get("orgName").enable(); + } + + if (!this.selfHosted || this.canEditSubscription) { + this.formGroup.get("billingEmail").enable(); + this.formGroup.get("businessName").enable(); + } + + this.loading = false; + }); } - async submit() { - try { - const request = new OrganizationUpdateRequest(); - request.name = this.org.name; - request.businessName = this.org.businessName; - request.billingEmail = this.org.billingEmail; + ngOnDestroy(): void { + // You must first call .next() in order for the notifier to properly close subscriptions using takeUntil + this.destroy$.next(); + this.destroy$.complete(); + } - // Backfill pub/priv key if necessary - if (!this.org.hasPublicAndPrivateKeys) { - const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); - const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); - request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); - } - - this.formPromise = this.organizationApiService.save(this.organizationId, request); - await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("organizationUpdated") - ); - } catch (e) { - this.logService.error(e); + submit = async () => { + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; } - } + + const request = new OrganizationUpdateRequest(); + request.name = this.formGroup.value.orgName; + request.businessName = this.formGroup.value.businessName; + request.billingEmail = this.formGroup.value.billingEmail; + + // Backfill pub/priv key if necessary + if (!this.org.hasPublicAndPrivateKeys) { + const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); + const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); + request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); + } + + this.formPromise = this.organizationApiService.save(this.organizationId, request); + await this.formPromise; + this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated")); + }; async deleteOrganization() { const dialog = openDeleteOrganizationDialog(this.dialogService, { diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index c5500c6a94a..9eaec0a068e 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; import { LooseComponentsModule, SharedModule } from "../../../shared"; +import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; import { PoliciesModule } from "../../organizations/policies"; import { AccountComponent } from "./account.component"; @@ -9,7 +10,13 @@ import { SettingsComponent } from "./settings.component"; import { TwoFactorSetupComponent } from "./two-factor-setup.component"; @NgModule({ - imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule], + imports: [ + SharedModule, + LooseComponentsModule, + PoliciesModule, + OrganizationSettingsRoutingModule, + AccountFingerprintComponent, + ], declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent], }) export class OrganizationSettingsModule {} diff --git a/apps/web/src/app/settings/profile.component.html b/apps/web/src/app/settings/profile.component.html index be0e58eaa1d..91e0d4b985a 100644 --- a/apps/web/src/app/settings/profile.component.html +++ b/apps/web/src/app/settings/profile.component.html @@ -46,19 +46,11 @@ Customize
-
-

- {{ "yourAccountsFingerprint" | i18n }}: - -
- {{ fingerprint }} -

+ +