1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 11:13:46 +00:00

[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 <smelton@bitwarden.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
Vincent Salucci
2023-06-15 21:03:48 -05:00
committed by GitHub
parent 0afbd90a2d
commit 5cd51374d7
11 changed files with 206 additions and 135 deletions

View File

@@ -1,95 +1,66 @@
<div class="page-header">
<h1>{{ "organizationInfo" | i18n }}</h1>
</div>
<h1 bitTypography="h1" class="tw-pb-2.5">{{ "organizationInfo" | i18n }}</h1>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<form
*ngIf="org && !loading"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{ "organizationName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="org.name"
[disabled]="selfHosted"
/>
</div>
<div class="form-group">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="org.billingEmail"
[disabled]="selfHosted || !canEditSubscription"
/>
</div>
<div class="form-group">
<label for="businessName">{{ "businessName" | i18n }}</label>
<input
id="businessName"
class="form-control"
type="text"
name="BusinessName"
[(ngModel)]="org.businessName"
[disabled]="selfHosted || !canEditSubscription"
/>
</div>
<form *ngIf="org && !loading" #form [bitSubmit]="submit" [formGroup]="formGroup">
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
<div>
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput id="orgName" type="text" formControlName="orgName" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput id="billingEmail" formControlName="billingEmail" type="email" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput id="businessName" formControlName="businessName" type="text" />
</bit-form-field>
</div>
<div class="col-6">
<div>
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
<app-account-fingerprint
[fingerprintMaterial]="organizationId"
[publicKeyBuffer]="publicKeyBuffer"
fingerprintLabel="{{ 'yourOrganizationsFingerprint' | i18n }}"
>
</app-account-fingerprint>
</div>
</div>
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "save" | i18n }}
</button>
</form>
<ng-container *ngIf="canUseApi">
<div class="secondary-header border-0 mb-0">
<h1>{{ "apiKey" | i18n }}</h1>
</div>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
<p>
{{ "apiKeyDesc" | i18n }}
<a href="https://docs.bitwarden.com" target="_blank" rel="noopener">
{{ "learnMore" | i18n }}
</a>
</p>
<button type="button" class="btn btn-outline-secondary" (click)="viewApiKey()">
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
{{ "viewApiKey" | i18n }}
</button>
<button type="button" class="btn btn-outline-secondary" (click)="rotateApiKey()">
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
{{ "rotateApiKey" | i18n }}
</button>
</ng-container>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{ "dangerZone" | i18n }}</h1>
</div>
<div class="card border-danger">
<div class="card-body">
<p>{{ "dangerZoneDesc" | i18n }}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()">
{{ "deleteOrganization" | i18n }}
</button>
<button type="button" class="btn btn-outline-danger" (click)="purgeVault()">
{{ "purgeVault" | i18n }}
</button>
</div>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>
<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-bg-background tw-p-5">
<p>{{ "dangerZoneDesc" | i18n }}</p>
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
{{ "deleteOrganization" | i18n }}
</button>
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
{{ "purgeVault" | i18n }}
</button>
</div>
<ng-template #purgeOrganizationTemplate></ng-template>
<ng-template #apiKeyTemplate></ng-template>

View File

@@ -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<OrganizationResponse>;
taxFormPromise: Promise<unknown>;
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<void>();
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, {

View File

@@ -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 {}