mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +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": {
|
"verifyDomain": {
|
||||||
"message": "Verify domain"
|
"message": "Verify domain"
|
||||||
},
|
},
|
||||||
|
"reverifyDomain": {
|
||||||
|
"message": "Reverify domain"
|
||||||
|
},
|
||||||
"copyDnsTxtRecord": {
|
"copyDnsTxtRecord": {
|
||||||
"message": "Copy DNS TXT record"
|
"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."
|
"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": {
|
"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": {
|
"domainRemoved": {
|
||||||
"message": "Domain removed"
|
"message": "Domain removed"
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
|
<form [formGroup]="domainForm" [bitSubmit]>
|
||||||
<span bitDialogTitle>
|
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
|
||||||
<span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span>
|
<span bitDialogTitle>
|
||||||
<span *ngIf="data.orgDomain"> {{ "verifyDomain" | i18n }}</span>
|
<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">{{
|
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">{{
|
||||||
data.orgDomain.domainName
|
data.orgDomain.domainName
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|
||||||
<!-- TODO: get status badge here -->
|
<span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge badgeType="warning"
|
||||||
</span>
|
>Unverified</span
|
||||||
<div bitDialogContent>
|
>
|
||||||
<form [formGroup]="domainForm" [bitSubmit]="verifyDomain">
|
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge badgeType="success"
|
||||||
<!-- <input bitInput formControlName="domainName" /> -->
|
>Verified</span
|
||||||
<!-- <span *ngIf="domainNameCtrl.touched && domainNameCtrl.invalid">Error</span> -->
|
>
|
||||||
|
</span>
|
||||||
<!-- TODO: investigate why bit-form-field breaks touched -->
|
<div bitDialogContent>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="domainName" />
|
<input bitInput formControlName="domainName" />
|
||||||
@@ -35,37 +36,40 @@
|
|||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
|
||||||
|
|
||||||
<bit-callout type="info" title="{{ 'automaticDomainVerification' | i18n }}">
|
<bit-callout type="info" title="{{ 'automaticDomainVerification' | i18n }}">
|
||||||
{{ "automaticDomainVerificationProcess" | i18n }}
|
{{ "automaticDomainVerificationProcess" | i18n }}
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
</div>
|
</div>
|
||||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-items-center tw-gap-2">
|
<div bitDialogFooter class="tw-flex tw-flex-row tw-items-center tw-gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="primary"
|
bitFormButton
|
||||||
[disabled]="domainForm.invalid"
|
buttonType="primary"
|
||||||
(click)="data.orgDomain ? verifyDomain() : saveDomain()"
|
[disabled]="domainForm.invalid"
|
||||||
[loading]="submitting"
|
[bitAction]="data.orgDomain ? verifyDomain : saveDomain"
|
||||||
>
|
>
|
||||||
{{ "verifyDomain" | i18n }}
|
<span *ngIf="!data?.orgDomain?.verifiedDate">{{ "verifyDomain" | i18n }}</span>
|
||||||
</button>
|
<span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span>
|
||||||
<button bitButton buttonType="secondary" [disabled]="submitting" (click)="dialogRef.close()">
|
</button>
|
||||||
{{ "cancel" | i18n }}
|
<button bitButton buttonType="secondary" (click)="dialogRef.close()">
|
||||||
</button>
|
{{ "cancel" | i18n }}
|
||||||
<button
|
</button>
|
||||||
*ngIf="data.orgDomain"
|
|
||||||
class="tw-ml-auto"
|
<button
|
||||||
bitIconButton="bwi-trash"
|
*ngIf="data.orgDomain"
|
||||||
buttonType="danger"
|
class="tw-ml-auto"
|
||||||
size="default"
|
bitIconButton="bwi-trash"
|
||||||
title="Delete"
|
buttonType="danger"
|
||||||
aria-label="Delete"
|
size="default"
|
||||||
(click)="deleteDomain()"
|
title="Delete"
|
||||||
[disabled]="submitting"
|
aria-label="Delete"
|
||||||
[loading]="deleting"
|
[bitAction]="deleteDomain"
|
||||||
></button>
|
type="submit"
|
||||||
</div>
|
bitButton
|
||||||
</bit-dialog>
|
bitFormButton
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
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 { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/abstractions/organization-domain/org-domain-api.service.abstraction";
|
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 { OrganizationDomainResponse } from "@bitwarden/common/abstractions/organization-domain/responses/organization-domain.response";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
@@ -22,11 +24,11 @@ export interface DomainAddEditDialogData {
|
|||||||
selector: "app-domain-add-edit-dialog",
|
selector: "app-domain-add-edit-dialog",
|
||||||
templateUrl: "domain-add-edit-dialog.component.html",
|
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";
|
dialogSize: "small" | "default" | "large" = "default";
|
||||||
disablePadding = false;
|
disablePadding = false;
|
||||||
|
|
||||||
// TODO: should invalidDomainNameMessage have something like: "'https://', 'http://', or 'www.' domain prefixes not allowed."
|
|
||||||
domainForm: FormGroup = this.formBuilder.group({
|
domainForm: FormGroup = this.formBuilder.group({
|
||||||
domainName: [
|
domainName: [
|
||||||
"",
|
"",
|
||||||
@@ -49,9 +51,6 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
return this.domainForm.controls.txt as FormControl;
|
return this.domainForm.controls.txt as FormControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting = false;
|
|
||||||
deleting = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialogRef: DialogRef,
|
public dialogRef: DialogRef,
|
||||||
@Inject(DIALOG_DATA) public data: DomainAddEditDialogData,
|
@Inject(DIALOG_DATA) public data: DomainAddEditDialogData,
|
||||||
@@ -59,7 +58,8 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
private cryptoFunctionService: CryptoFunctionServiceAbstraction,
|
private cryptoFunctionService: CryptoFunctionServiceAbstraction,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private orgDomainApiService: OrgDomainApiServiceAbstraction
|
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||||
|
private orgDomainService: OrgDomainServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -84,15 +84,20 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
)}`;
|
)}`;
|
||||||
this.txtCtrl.setValue(generatedTxt);
|
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 {
|
copyDnsTxt(): void {
|
||||||
this.platformUtilsService.copyToClipboard(this.txtCtrl.value);
|
this.orgDomainService.copyDnsTxt(this.txtCtrl.value);
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: error handling?
|
// TODO: error handling?
|
||||||
@@ -104,8 +109,7 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
// If verified, no action can be taken but delete
|
// If verified, no action can be taken but delete
|
||||||
// If saved & unverified, can prompt verification
|
// If saved & unverified, can prompt verification
|
||||||
|
|
||||||
async saveDomain(): Promise<void> {
|
saveDomain = async (): Promise<void> => {
|
||||||
this.submitting = true;
|
|
||||||
this.domainForm.disable();
|
this.domainForm.disable();
|
||||||
|
|
||||||
const request: OrganizationDomainRequest = new OrganizationDomainRequest(
|
const request: OrganizationDomainRequest = new OrganizationDomainRequest(
|
||||||
@@ -118,14 +122,12 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
//TODO: figure out how to handle DomainVerifiedException
|
//TODO: figure out how to handle DomainVerifiedException
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved"));
|
||||||
this.submitting = false;
|
|
||||||
|
|
||||||
// TODO: verify before closing modal; close if successful
|
// TODO: verify before closing modal; close if successful
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
};
|
||||||
|
|
||||||
async verifyDomain(): Promise<void> {
|
verifyDomain = async (): Promise<void> => {
|
||||||
this.submitting = true;
|
|
||||||
this.domainForm.disable();
|
this.domainForm.disable();
|
||||||
|
|
||||||
const success: boolean = await this.orgDomainApiService.verify(
|
const success: boolean = await this.orgDomainApiService.verify(
|
||||||
@@ -133,8 +135,6 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
this.data.orgDomain.id
|
this.data.orgDomain.id
|
||||||
);
|
);
|
||||||
|
|
||||||
this.submitting = false;
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified"));
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
@@ -149,15 +149,19 @@ export class DomainAddEditDialogComponent implements OnInit {
|
|||||||
// Someone else is using [domain]. Use a different domain to continue.
|
// 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.
|
// I only have a bool to indicate success or failure.. not why it failed.
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async deleteDomain(): Promise<void> {
|
deleteDomain = async (): Promise<void> => {
|
||||||
// TODO: Do I need an are you sure prompt?
|
// TODO: Do I need an are you sure prompt? yes
|
||||||
|
|
||||||
this.deleting = true;
|
|
||||||
await this.orgDomainApiService.delete(this.data.organizationId, this.data.orgDomain.id);
|
await this.orgDomainApiService.delete(this.data.organizationId, this.data.orgDomain.id);
|
||||||
this.deleting = false;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved"));
|
||||||
this.dialogRef.close();
|
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>
|
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<bit-menu #orgDomainOptions>
|
<bit-menu #orgDomainOptions>
|
||||||
<button bitMenuItem>
|
<button bitMenuItem (click)="copyDnsTxt(orgDomain.txt)">
|
||||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||||
Copy DNS TXT record
|
Copy DNS TXT record
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
|
|||||||
return existingDomainNames;
|
return existingDomainNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Options
|
||||||
|
|
||||||
|
copyDnsTxt(dnsTxt: string): void {
|
||||||
|
this.orgDomainService.copyDnsTxt(dnsTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.componentDestroyed$.next();
|
this.componentDestroyed$.next();
|
||||||
this.componentDestroyed$.complete();
|
this.componentDestroyed$.complete();
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
{
|
{
|
||||||
provide: OrgDomainServiceAbstraction,
|
provide: OrgDomainServiceAbstraction,
|
||||||
useClass: OrgDomainService,
|
useClass: OrgDomainService,
|
||||||
deps: [],
|
deps: [PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: OrgDomainInternalServiceAbstraction,
|
provide: OrgDomainInternalServiceAbstraction,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export abstract class OrgDomainServiceAbstraction {
|
|||||||
orgDomains$: Observable<OrganizationDomainResponse[]>;
|
orgDomains$: Observable<OrganizationDomainResponse[]>;
|
||||||
|
|
||||||
get: (orgDomainId: string) => Promise<OrganizationDomainResponse>;
|
get: (orgDomainId: string) => Promise<OrganizationDomainResponse>;
|
||||||
|
|
||||||
|
copyDnsTxt: (dnsTxt: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this separate class is designed to hold methods that are not
|
// 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 { OrgDomainInternalServiceAbstraction } from "../../abstractions/organization-domain/org-domain.service.abstraction";
|
||||||
import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response";
|
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 {
|
export class OrgDomainService implements OrgDomainInternalServiceAbstraction {
|
||||||
protected _orgDomains$: BehaviorSubject<OrganizationDomainResponse[]> = new BehaviorSubject([]);
|
protected _orgDomains$: BehaviorSubject<OrganizationDomainResponse[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
orgDomains$ = this._orgDomains$.asObservable();
|
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> {
|
async get(orgDomainId: string): Promise<OrganizationDomainResponse> {
|
||||||
const orgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue();
|
const orgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue();
|
||||||
@@ -17,6 +21,15 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction {
|
|||||||
return orgDomains.find((orgDomain) => orgDomain.id === orgDomainId);
|
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 {
|
upsert(orgDomains: OrganizationDomainResponse[]): void {
|
||||||
const existingOrgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue();
|
const existingOrgDomains: OrganizationDomainResponse[] = this._orgDomains$.getValue();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user