1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 23:03:32 +00:00

SG-680 - Domain verification progress - (1) Table layout + loading working for the most part (more translations needed (2) Add & edit opening dialog (3) Dialog first draft of save and verify

This commit is contained in:
Jared Snider
2022-12-08 17:53:37 -05:00
parent 19e7513d65
commit 64daf70cea
8 changed files with 237 additions and 64 deletions

View File

@@ -5557,7 +5557,25 @@
"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": "'https://', 'http://', or 'www.' domain prefixes not allowed."
"message": "Input is not a valid format. Example: mydomain.com. Subdomains require separate entries to be verified."
},
"domainRemoved": {
"message": "Domain removed"
},
"domainSaved": {
"message": "Domain saved"
},
"domainVerified": {
"message": "Domain verified"
},
"domainNotVerified": {
"message": "$DOMAIN$ not verified. Check your DNS record.",
"placeholders": {
"DOMAIN": {
"content": "$1",
"example": "bitwarden.com"
}
}
}
}

View File

@@ -1,7 +1,20 @@
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>{{ "newDomain" | i18n }}</span>
<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>
<!-- TODO: get status badge here -->
</span>
<div bitDialogContent>
<form [formGroup]="domainForm">
<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 -->
<bit-form-field>
<bit-label>{{ "domainName" | i18n }}</bit-label>
<input bitInput formControlName="domainName" />
@@ -29,10 +42,17 @@
</bit-callout>
</div>
<div bitDialogFooter class="tw-flex tw-flex-row tw-items-center tw-gap-2">
<button bitButton buttonType="primary" [disabled]="domainForm.invalid">
<button
type="submit"
bitButton
buttonType="primary"
[disabled]="domainForm.invalid"
(click)="data.orgDomain ? verifyDomain() : saveDomain()"
[loading]="submitting"
>
{{ "verifyDomain" | i18n }}
</button>
<button bitButton buttonType="secondary" (click)="dialogRef.close()">
<button bitButton buttonType="secondary" [disabled]="submitting" (click)="dialogRef.close()">
{{ "cancel" | i18n }}
</button>
<button
@@ -43,6 +63,9 @@
size="default"
title="Delete"
aria-label="Delete"
(click)="deleteDomain()"
[disabled]="submitting"
[loading]="deleting"
></button>
</div>
</bit-dialog>

View File

@@ -4,14 +4,17 @@ import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"
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 { 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";
import { OrganizationDomainRequest } from "@bitwarden/common/services/organization-domain/requests/organization-domain.request";
import { domainNameValidator } from "./domain-name.validator";
export interface DomainAddEditDialogData {
organizationId: string;
orgDomain: OrganizationDomainResponse;
existingDomainNames: Array<string>;
}
@Component({
@@ -22,6 +25,8 @@ export class DomainAddEditDialogComponent implements OnInit {
dialogSize: "small" | "default" | "large" = "default";
disablePadding = false;
// TODO: should invalidDomainNameMessage have something like: "'https://', 'http://', or 'www.' domain prefixes not allowed."
// TODO: write separate uniqueIn validator w/ translated msg: "You cant claim the same domain twice."
domainForm: FormGroup = this.formBuilder.group({
domainName: [
"",
@@ -37,13 +42,17 @@ 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,
private formBuilder: FormBuilder,
private cryptoFunctionService: CryptoFunctionServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
private i18nService: I18nService,
private orgDomainApiService: OrgDomainApiServiceAbstraction
) {}
async ngOnInit(): Promise<void> {
@@ -55,6 +64,7 @@ export class DomainAddEditDialogComponent implements OnInit {
if (this.data.orgDomain) {
// Edit
this.domainForm.patchValue(this.data.orgDomain);
this.domainForm.disable();
} else {
// Add
@@ -69,12 +79,78 @@ export class DomainAddEditDialogComponent implements OnInit {
}
}
copyDnsTxt() {
copyDnsTxt(): void {
this.platformUtilsService.copyToClipboard(this.txtCtrl.value);
this.platformUtilsService.showToast(
"info",
"success",
null,
this.i18nService.t("valueCopied", this.i18nService.t("dnsTxtRecord"))
);
}
// TODO: error handling?
// TODO: probably will be a need to split into different actions: save == save + verify
// and if edit true, then verify is verify.
// Need to display verified status somewhere
// If verified, no action can be taken but delete
// If saved & unverified, can prompt verification
async saveDomain(): Promise<void> {
this.submitting = true;
this.domainForm.disable();
const request: OrganizationDomainRequest = new OrganizationDomainRequest(
this.txtCtrl.value,
this.domainNameCtrl.value
);
await this.orgDomainApiService.post(this.data.organizationId, request);
//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;
this.domainForm.disable();
const success: boolean = await this.orgDomainApiService.verify(
this.data.organizationId,
this.data.orgDomain.id
);
this.submitting = false;
if (success) {
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified"));
this.dialogRef.close();
} else {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("domainNotVerified", this.domainNameCtrl.value)
);
// TODO: discuss with Danielle / Gbubemi:
// 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?
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();
}
}

View File

@@ -15,12 +15,8 @@
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<!-- <div *ngIf="orgDomains$ | async as orgDomains; else loading">
{{ obs }}
</div>
<ng-template #loading>Loading...</ng-template> -->
<ng-container *ngIf="!loading">
<ng-container *ngIf="orgDomains$ | async as orgDomains">
<div class="tw-flex tw-flex-row">
<bit-table class="tw-w-full tw-table-auto">
<ng-container header>
@@ -33,19 +29,56 @@
</ng-container>
<ng-container body>
<!-- [alignContent]="alignRowContent" -->
<tr bitRow *ngFor="let orgDomain of orgDomains$ | async; index as i">
<td bitCell>{{ orgDomain.domainName }}</td>
<tr bitRow *ngFor="let orgDomain of orgDomains; index as i">
<td bitCell>
{{ orgDomain.domainName }}
<a bitLink href appStopClick linkType="primary" (click)="editDomain(orgDomain)">{{
orgDomain.domainName
}}</a>
</td>
<td bitCell>
<span *ngIf="!orgDomain?.verifiedDate" bitBadge badgeType="warning">Unverified</span>
<span *ngIf="orgDomain?.verifiedDate" bitBadge badgeType="success">Verified</span>
</td>
<td bitCell>
<!-- TODO: next run date != last checked... -->
{{ orgDomain.nextRunDate | date: "medium" }}
</td>
<td bitCell class="table-list-options">
<button
[bitMenuTriggerFor]="orgDomainOptions"
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #orgDomainOptions>
<button bitMenuItem>
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
Copy DNS TXT record
</button>
<button bitMenuItem>
<i class="bwi bwi-fw bwi-check text-success" aria-hidden="true"></i>
Verify domain
</button>
<button bitMenuItem>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</span>
</button>
</bit-menu>
</td>
<td bitCell>test</td>
<td bitCell>test</td>
</tr>
</ng-container>
</bit-table>
</div>
<div class="tw-mt-6 tw-flex tw-flex-col tw-items-center tw-justify-center">
<div
class="tw-mt-6 tw-flex tw-flex-col tw-items-center tw-justify-center"
*ngIf="orgDomains?.length == 0"
>
<img src="../../images/domain-verification/domain.svg" class="tw-mb-4" alt="domain" />
<div class="tw-mb-2 tw-flex tw-flex-row tw-justify-center">
@@ -62,5 +95,5 @@
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> {{ "newDomain" | i18n }}
</button>
</div>
¸
</ng-container>
</ng-container>

View File

@@ -68,6 +68,19 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
const domainAddEditDialogData: DomainAddEditDialogData = {
organizationId: this.organizationId,
orgDomain: null,
existingDomainNames: [],
};
this.dialogService.open(DomainAddEditDialogComponent, {
data: domainAddEditDialogData,
});
}
editDomain(orgDomain: OrganizationDomainResponse) {
const domainAddEditDialogData: DomainAddEditDialogData = {
organizationId: this.organizationId,
orgDomain: orgDomain,
existingDomainNames: [],
};
this.dialogService.open(DomainAddEditDialogComponent, {

View File

@@ -1,3 +1,5 @@
import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request";
import { OrganizationDomainResponse } from "./responses/organization-domain.response";
export abstract class OrgDomainApiServiceAbstraction {
@@ -6,7 +8,10 @@ export abstract class OrgDomainApiServiceAbstraction {
orgId: string,
orgDomainId: string
) => Promise<OrganizationDomainResponse>;
post: (orgId: string, orgDomain: OrganizationDomainResponse) => Promise<any>;
post: (
orgId: string,
orgDomain: OrganizationDomainRequest
) => Promise<OrganizationDomainResponse>;
verify: (orgId: string, orgDomainId: string) => Promise<boolean>;
delete: (orgId: string, orgDomainId: string) => Promise<any>;
}

View File

@@ -45,13 +45,14 @@ export class OrgDomainApiService implements OrgDomainApiServiceAbstraction {
return response;
}
async post(orgId: string, orgDomain: OrganizationDomainResponse): Promise<any> {
const request = new OrganizationDomainRequest(orgDomain);
async post(
orgId: string,
orgDomainReq: OrganizationDomainRequest
): Promise<OrganizationDomainResponse> {
const result = await this.apiService.send(
"POST",
`/organizations/${orgId}`,
request,
`/organizations/${orgId}/domain`,
orgDomainReq,
true,
true
);
@@ -66,7 +67,7 @@ export class OrgDomainApiService implements OrgDomainApiServiceAbstraction {
async verify(orgId: string, orgDomainId: string): Promise<boolean> {
const result: boolean = await this.apiService.send(
"POST",
`/organizations/${orgId}/${orgDomainId}/verify`,
`/organizations/${orgId}/domain/${orgDomainId}/verify`,
null,
true,
true
@@ -76,7 +77,13 @@ export class OrgDomainApiService implements OrgDomainApiServiceAbstraction {
}
async delete(orgId: string, orgDomainId: string): Promise<any> {
this.apiService.send("DELETE", `/organizations/${orgId}/${orgDomainId}`, null, true, false);
this.apiService.send(
"DELETE",
`/organizations/${orgId}/domain/${orgDomainId}`,
null,
true,
false
);
this.orgDomainService.delete([orgDomainId]);
}

View File

@@ -1,11 +1,9 @@
import { OrganizationDomainResponse } from "../../../abstractions/organization-domain/responses/organization-domain.response";
export class OrganizationDomainRequest {
txt: string;
domainName: string;
constructor(orgDomainResponse: OrganizationDomainResponse) {
this.txt = orgDomainResponse.txt;
this.domainName = orgDomainResponse.domainName;
constructor(txt: string, domainName: string) {
this.txt = txt;
this.domainName = domainName;
}
}