diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8325d905e18..e24cfb62f6b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5538,6 +5538,9 @@ "verifyDomain": { "message": "Verify domain" }, + "reverifyDomain": { + "message": "Reverify domain" + }, "copyDnsTxtRecord": { "message": "Copy DNS TXT record" }, @@ -5557,7 +5560,7 @@ "message": "Bitwarden will attempt to verify the domain 3 times during the first 72 hours. If the domain can’t be verified, check the DNS record in your host and manually verify." }, "invalidDomainNameMessage": { - "message": "Input is not a valid format. Example: mydomain.com. Subdomains require separate entries to be verified." + "message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be verified." }, "domainRemoved": { "message": "Domain removed" diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index 8f70f9752f5..a7c8b401e39 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -1,20 +1,21 @@ - - - {{ "newDomain" | i18n }} - {{ "verifyDomain" | i18n }} +
+ + + {{ "newDomain" | i18n }} + {{ "verifyDomain" | i18n }} - {{ - data.orgDomain.domainName - }} + {{ + data.orgDomain.domainName + }} - - -
- - - - - + Unverified + Verified + +
{{ "domainName" | i18n }} @@ -35,37 +36,40 @@ - - - {{ "automaticDomainVerificationProcess" | i18n }} - -
-
- - - -
- + + {{ "automaticDomainVerificationProcess" | i18n }} + +
+
+ + + + +
+
+ diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index 4fcc521ee64..6ea6c5ea68f 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -1,10 +1,12 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit } from "@angular/core"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/abstractions/organization-domain/org-domain-api.service.abstraction"; +import { OrgDomainServiceAbstraction } from "@bitwarden/common/abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainResponse } from "@bitwarden/common/abstractions/organization-domain/responses/organization-domain.response"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { Utils } from "@bitwarden/common/misc/utils"; @@ -22,11 +24,11 @@ export interface DomainAddEditDialogData { selector: "app-domain-add-edit-dialog", templateUrl: "domain-add-edit-dialog.component.html", }) -export class DomainAddEditDialogComponent implements OnInit { +export class DomainAddEditDialogComponent implements OnInit, OnDestroy { + private componentDestroyed$: Subject = new Subject(); dialogSize: "small" | "default" | "large" = "default"; disablePadding = false; - // TODO: should invalidDomainNameMessage have something like: "'https://', 'http://', or 'www.' domain prefixes not allowed." domainForm: FormGroup = this.formBuilder.group({ domainName: [ "", @@ -49,9 +51,6 @@ export class DomainAddEditDialogComponent implements OnInit { return this.domainForm.controls.txt as FormControl; } - submitting = false; - deleting = false; - constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: DomainAddEditDialogData, @@ -59,7 +58,8 @@ export class DomainAddEditDialogComponent implements OnInit { private cryptoFunctionService: CryptoFunctionServiceAbstraction, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private orgDomainApiService: OrgDomainApiServiceAbstraction + private orgDomainApiService: OrgDomainApiServiceAbstraction, + private orgDomainService: OrgDomainServiceAbstraction ) {} async ngOnInit(): Promise { @@ -84,15 +84,20 @@ export class DomainAddEditDialogComponent implements OnInit { )}`; this.txtCtrl.setValue(generatedTxt); } + + this.setupFormListeners(); + } + + setupFormListeners(): void { + // By default, suppresses touched state on change for reactive form control inputs + // I want validation errors to be shown as the user types (as validators are running on change anyhow by default). + this.domainForm.valueChanges.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => { + this.domainForm.markAllAsTouched(); + }); } copyDnsTxt(): void { - this.platformUtilsService.copyToClipboard(this.txtCtrl.value); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")) - ); + this.orgDomainService.copyDnsTxt(this.txtCtrl.value); } // TODO: error handling? @@ -104,8 +109,7 @@ export class DomainAddEditDialogComponent implements OnInit { // If verified, no action can be taken but delete // If saved & unverified, can prompt verification - async saveDomain(): Promise { - this.submitting = true; + saveDomain = async (): Promise => { this.domainForm.disable(); const request: OrganizationDomainRequest = new OrganizationDomainRequest( @@ -118,14 +122,12 @@ export class DomainAddEditDialogComponent implements OnInit { //TODO: figure out how to handle DomainVerifiedException this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved")); - this.submitting = false; // TODO: verify before closing modal; close if successful this.dialogRef.close(); - } + }; - async verifyDomain(): Promise { - this.submitting = true; + verifyDomain = async (): Promise => { this.domainForm.disable(); const success: boolean = await this.orgDomainApiService.verify( @@ -133,8 +135,6 @@ export class DomainAddEditDialogComponent implements OnInit { this.data.orgDomain.id ); - this.submitting = false; - if (success) { this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified")); this.dialogRef.close(); @@ -149,15 +149,19 @@ export class DomainAddEditDialogComponent implements OnInit { // Someone else is using [domain]. Use a different domain to continue. // I only have a bool to indicate success or failure.. not why it failed. } - } + }; - async deleteDomain(): Promise { - // TODO: Do I need an are you sure prompt? + deleteDomain = async (): Promise => { + // TODO: Do I need an are you sure prompt? yes - this.deleting = true; await this.orgDomainApiService.delete(this.data.organizationId, this.data.orgDomain.id); - this.deleting = false; + this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved")); this.dialogRef.close(); + }; + + ngOnDestroy(): void { + this.componentDestroyed$.next(); + this.componentDestroyed$.complete(); } } diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.html index 825f58e27ff..eaf5aba9df7 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.html @@ -54,7 +54,7 @@ - diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.ts index e8b9b4012cd..e4d6655afbf 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/organizations/manage/domain-verification/domain-verification.component.ts @@ -97,6 +97,14 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { return existingDomainNames; } + //#region Options + + copyDnsTxt(dnsTxt: string): void { + this.orgDomainService.copyDnsTxt(dnsTxt); + } + + //#endregion + ngOnDestroy(): void { this.componentDestroyed$.next(); this.componentDestroyed$.complete(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 48c4a8fcd21..1e91ef23293 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -609,7 +609,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; { provide: OrgDomainServiceAbstraction, useClass: OrgDomainService, - deps: [], + deps: [PlatformUtilsServiceAbstraction, I18nServiceAbstraction], }, { provide: OrgDomainInternalServiceAbstraction, diff --git a/libs/common/src/abstractions/organization-domain/org-domain.service.abstraction.ts b/libs/common/src/abstractions/organization-domain/org-domain.service.abstraction.ts index da8ecc7192e..9b020e4a4ca 100644 --- a/libs/common/src/abstractions/organization-domain/org-domain.service.abstraction.ts +++ b/libs/common/src/abstractions/organization-domain/org-domain.service.abstraction.ts @@ -6,6 +6,8 @@ export abstract class OrgDomainServiceAbstraction { orgDomains$: Observable; get: (orgDomainId: string) => Promise; + + copyDnsTxt: (dnsTxt: string) => void; } // Note: this separate class is designed to hold methods that are not diff --git a/libs/common/src/services/organization-domain/org-domain.service.ts b/libs/common/src/services/organization-domain/org-domain.service.ts index 0e270fbf48b..9611bd076e9 100644 --- a/libs/common/src/services/organization-domain/org-domain.service.ts +++ b/libs/common/src/services/organization-domain/org-domain.service.ts @@ -2,14 +2,18 @@ import { BehaviorSubject } from "rxjs"; import { OrgDomainInternalServiceAbstraction } from "../../abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; +import { PlatformUtilsService } from "../../abstractions/platformUtils.service"; +import { I18nService } from "../i18n.service"; export class OrgDomainService implements OrgDomainInternalServiceAbstraction { protected _orgDomains$: BehaviorSubject = new BehaviorSubject([]); orgDomains$ = this._orgDomains$.asObservable(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor() {} + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService + ) {} async get(orgDomainId: string): Promise { const orgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue(); @@ -17,6 +21,15 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction { return orgDomains.find((orgDomain) => orgDomain.id === orgDomainId); } + copyDnsTxt(dnsTxt: string): void { + this.platformUtilsService.copyToClipboard(dnsTxt); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord")) + ); + } + upsert(orgDomains: OrganizationDomainResponse[]): void { const existingOrgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue();