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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -609,7 +609,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
{
|
||||
provide: OrgDomainServiceAbstraction,
|
||||
useClass: OrgDomainService,
|
||||
deps: [],
|
||||
deps: [PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: OrgDomainInternalServiceAbstraction,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user