-
-
{{ "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
-