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:
@@ -5557,7 +5557,25 @@
|
||||
"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": "'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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 can’t 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user