1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

SG-680 - Domain verification progress - (1) CopyDnsTxt added to state service (2) Refactored dialog to use async actions (3) Dialog form changes now mark form controls as touched for more responsive error handling

This commit is contained in:
Jared Snider
2022-12-09 16:53:29 -05:00
parent 964fab1782
commit 976d7c29b0
8 changed files with 113 additions and 79 deletions

View File

@@ -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 cant 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"

View File

@@ -1,20 +1,21 @@
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>
<span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span>
<span *ngIf="data.orgDomain"> {{ "verifyDomain" | i18n }}</span>
<form [formGroup]="domainForm" [bitSubmit]>
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>
<span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span>
<span *ngIf="data.orgDomain"> {{ "verifyDomain" | i18n }}</span>
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">{{
data.orgDomain.domainName
}}</span>
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">{{
data.orgDomain.domainName
}}</span>
<!-- TODO: get status badge here -->
</span>
<div bitDialogContent>
<form [formGroup]="domainForm" [bitSubmit]="verifyDomain">
<!-- <input bitInput formControlName="domainName" /> -->
<!-- <span *ngIf="domainNameCtrl.touched && domainNameCtrl.invalid">Error</span> -->
<!-- TODO: investigate why bit-form-field breaks touched -->
<span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge badgeType="warning"
>Unverified</span
>
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge badgeType="success"
>Verified</span
>
</span>
<div bitDialogContent>
<bit-form-field>
<bit-label>{{ "domainName" | i18n }}</bit-label>
<input bitInput formControlName="domainName" />
@@ -35,37 +36,40 @@
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
</form>
<bit-callout type="info" title="{{ 'automaticDomainVerification' | i18n }}">
{{ "automaticDomainVerificationProcess" | i18n }}
</bit-callout>
</div>
<div bitDialogFooter class="tw-flex tw-flex-row tw-items-center tw-gap-2">
<button
type="submit"
bitButton
buttonType="primary"
[disabled]="domainForm.invalid"
(click)="data.orgDomain ? verifyDomain() : saveDomain()"
[loading]="submitting"
>
{{ "verifyDomain" | i18n }}
</button>
<button bitButton buttonType="secondary" [disabled]="submitting" (click)="dialogRef.close()">
{{ "cancel" | i18n }}
</button>
<button
*ngIf="data.orgDomain"
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"
(click)="deleteDomain()"
[disabled]="submitting"
[loading]="deleting"
></button>
</div>
</bit-dialog>
<bit-callout type="info" title="{{ 'automaticDomainVerification' | i18n }}">
{{ "automaticDomainVerificationProcess" | i18n }}
</bit-callout>
</div>
<div bitDialogFooter class="tw-flex tw-flex-row tw-items-center tw-gap-2">
<button
type="submit"
bitButton
bitFormButton
buttonType="primary"
[disabled]="domainForm.invalid"
[bitAction]="data.orgDomain ? verifyDomain : saveDomain"
>
<span *ngIf="!data?.orgDomain?.verifiedDate">{{ "verifyDomain" | i18n }}</span>
<span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span>
</button>
<button bitButton buttonType="secondary" (click)="dialogRef.close()">
{{ "cancel" | i18n }}
</button>
<button
*ngIf="data.orgDomain"
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"
[bitAction]="deleteDomain"
type="submit"
bitButton
bitFormButton
></button>
</div>
</bit-dialog>
</form>

View File

@@ -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<void> = 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<void> {
@@ -84,15 +84,20 @@ export class DomainAddEditDialogComponent implements OnInit {
)}`;
this.txtCtrl.setValue(generatedTxt);
}
this.setupFormListeners();
}
setupFormListeners(): void {
// By default, <bit-form-field> 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<void> {
this.submitting = true;
saveDomain = async (): Promise<void> => {
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<void> {
this.submitting = true;
verifyDomain = async (): Promise<void> => {
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<void> {
// TODO: Do I need an are you sure prompt?
deleteDomain = async (): Promise<void> => {
// 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();
}
}

View File

@@ -54,7 +54,7 @@
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #orgDomainOptions>
<button bitMenuItem>
<button bitMenuItem (click)="copyDnsTxt(orgDomain.txt)">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
Copy DNS TXT record
</button>

View File

@@ -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();

View File

@@ -609,7 +609,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
{
provide: OrgDomainServiceAbstraction,
useClass: OrgDomainService,
deps: [],
deps: [PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
},
{
provide: OrgDomainInternalServiceAbstraction,

View File

@@ -6,6 +6,8 @@ export abstract class OrgDomainServiceAbstraction {
orgDomains$: Observable<OrganizationDomainResponse[]>;
get: (orgDomainId: string) => Promise<OrganizationDomainResponse>;
copyDnsTxt: (dnsTxt: string) => void;
}
// Note: this separate class is designed to hold methods that are not

View File

@@ -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<OrganizationDomainResponse[]> = 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<OrganizationDomainResponse> {
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();