mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 03:03:43 +00:00
refactor: move manage domain verification to admin-console, refs AC-1202 (#5077)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<form
|
||||
[formGroup]="domainForm"
|
||||
[bitSubmit]="data.orgDomain ? verifyDomain : saveDomain"
|
||||
[allowDisabledFormSubmit]="true"
|
||||
>
|
||||
<bit-dialog [dialogSize]="'default'" [disablePadding]="false">
|
||||
<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 && !data.orgDomain?.verifiedDate" bitBadge badgeType="warning">{{
|
||||
"domainStatusUnverified" | i18n
|
||||
}}</span>
|
||||
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge badgeType="success">{{
|
||||
"domainStatusVerified" | i18n
|
||||
}}</span>
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||
<input bitInput appAutofocus formControlName="domainName" [showErrorsWhenDisabled]="true" />
|
||||
<bit-hint>{{ "domainNameInputHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "dnsTxtRecord" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="txt" />
|
||||
<bit-hint>{{ "dnsTxtRecordInputHint" | i18n }}</bit-hint>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitButton
|
||||
appA11yTitle="{{ 'copyDnsTxtRecord' | i18n }}"
|
||||
(click)="copyDnsTxt()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-callout
|
||||
*ngIf="!data?.orgDomain?.verifiedDate"
|
||||
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">
|
||||
<span *ngIf="!data?.orgDomain?.verifiedDate">{{ "verifyDomain" | i18n }}</span>
|
||||
<span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="data.orgDomain"
|
||||
class="tw-ml-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
size="default"
|
||||
title="{{ 'delete' | i18n }}"
|
||||
aria-label="Delete"
|
||||
[bitAction]="deleteDomain"
|
||||
type="submit"
|
||||
bitFormButton
|
||||
></button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,270 @@
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, FormGroup, ValidatorFn, 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 { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { OrganizationDomainRequest } from "@bitwarden/common/services/organization-domain/requests/organization-domain.request";
|
||||
|
||||
import { domainNameValidator } from "./validators/domain-name.validator";
|
||||
import { uniqueInArrayValidator } from "./validators/unique-in-array.validator";
|
||||
export interface DomainAddEditDialogData {
|
||||
organizationId: string;
|
||||
orgDomain: OrganizationDomainResponse;
|
||||
existingDomainNames: Array<string>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-domain-add-edit-dialog",
|
||||
templateUrl: "domain-add-edit-dialog.component.html",
|
||||
})
|
||||
export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
|
||||
private componentDestroyed$: Subject<void> = new Subject();
|
||||
|
||||
domainForm: FormGroup = this.formBuilder.group({
|
||||
domainName: [
|
||||
"",
|
||||
[
|
||||
Validators.required,
|
||||
domainNameValidator(this.i18nService.t("invalidDomainNameMessage")),
|
||||
uniqueInArrayValidator(
|
||||
this.data.existingDomainNames,
|
||||
this.i18nService.t("duplicateDomainError")
|
||||
),
|
||||
],
|
||||
],
|
||||
txt: [{ value: null, disabled: true }],
|
||||
});
|
||||
|
||||
get domainNameCtrl(): FormControl {
|
||||
return this.domainForm.controls.domainName as FormControl;
|
||||
}
|
||||
get txtCtrl(): FormControl {
|
||||
return this.domainForm.controls.txt as FormControl;
|
||||
}
|
||||
|
||||
rejectedDomainNameValidator: ValidatorFn = null;
|
||||
|
||||
rejectedDomainNames: Array<string> = [];
|
||||
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) public data: DomainAddEditDialogData,
|
||||
private formBuilder: FormBuilder,
|
||||
private cryptoFunctionService: CryptoFunctionServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||
private orgDomainService: OrgDomainServiceAbstraction,
|
||||
private validationService: ValidationService
|
||||
) {}
|
||||
|
||||
//#region Angular Method Implementations
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
// If we have data.orgDomain, then editing, otherwise creating new domain
|
||||
await this.populateForm();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.componentDestroyed$.next();
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Form methods
|
||||
|
||||
async populateForm(): Promise<void> {
|
||||
if (this.data.orgDomain) {
|
||||
// Edit
|
||||
this.domainForm.patchValue(this.data.orgDomain);
|
||||
this.domainForm.disable();
|
||||
} else {
|
||||
// Add
|
||||
|
||||
// Figuring out the proper length of our DNS TXT Record value was fun.
|
||||
// DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
|
||||
// Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802
|
||||
// So, chose a magic # of 33 bytes to achieve at least that once converted to base 64 (47 char length).
|
||||
const generatedTxt = `bw=${Utils.fromBufferToB64(
|
||||
await this.cryptoFunctionService.randomBytes(33)
|
||||
)}`;
|
||||
this.txtCtrl.setValue(generatedTxt);
|
||||
}
|
||||
|
||||
this.setupFormListeners();
|
||||
}
|
||||
|
||||
setupFormListeners(): void {
|
||||
// <bit-form-field> suppresses touched state on change for reactive form controls
|
||||
// Manually set touched to show validation errors as the user stypes
|
||||
this.domainForm.valueChanges.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
|
||||
this.domainForm.markAllAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
copyDnsTxt(): void {
|
||||
this.orgDomainService.copyDnsTxt(this.txtCtrl.value);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Async Form Actions
|
||||
saveDomain = async (): Promise<void> => {
|
||||
if (this.domainForm.invalid) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.domainNameCtrl.disable();
|
||||
|
||||
const request: OrganizationDomainRequest = new OrganizationDomainRequest(
|
||||
this.txtCtrl.value,
|
||||
this.domainNameCtrl.value
|
||||
);
|
||||
|
||||
try {
|
||||
this.data.orgDomain = await this.orgDomainApiService.post(this.data.organizationId, request);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved"));
|
||||
await this.verifyDomain();
|
||||
} catch (e) {
|
||||
this.handleDomainSaveError(e);
|
||||
}
|
||||
};
|
||||
|
||||
private handleDomainSaveError(e: any): void {
|
||||
if (e instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = e as ErrorResponse;
|
||||
switch (errorResponse.statusCode) {
|
||||
case HttpStatusCode.Conflict:
|
||||
if (errorResponse.message.includes("The domain is not available to be claimed")) {
|
||||
// If user has attempted to claim a different rejected domain first:
|
||||
if (this.rejectedDomainNameValidator) {
|
||||
// Remove the validator:
|
||||
this.domainNameCtrl.removeValidators(this.rejectedDomainNameValidator);
|
||||
this.domainNameCtrl.updateValueAndValidity();
|
||||
}
|
||||
|
||||
// Update rejected domain names and add new unique in validator
|
||||
// which will prevent future known bad domain name submissions.
|
||||
this.rejectedDomainNames.push(this.domainNameCtrl.value);
|
||||
|
||||
this.rejectedDomainNameValidator = uniqueInArrayValidator(
|
||||
this.rejectedDomainNames,
|
||||
this.i18nService.t("domainNotAvailable", this.domainNameCtrl.value)
|
||||
);
|
||||
|
||||
this.domainNameCtrl.addValidators(this.rejectedDomainNameValidator);
|
||||
this.domainNameCtrl.updateValueAndValidity();
|
||||
|
||||
// Give them another chance to enter a new domain name:
|
||||
this.domainForm.enable();
|
||||
} else {
|
||||
this.validationService.showError(errorResponse);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
this.validationService.showError(errorResponse);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
}
|
||||
|
||||
verifyDomain = async (): Promise<void> => {
|
||||
if (this.domainForm.invalid) {
|
||||
// Note: shouldn't be possible, but going to leave this to be safe.
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.data.orgDomain = await this.orgDomainApiService.verify(
|
||||
this.data.organizationId,
|
||||
this.data.orgDomain.id
|
||||
);
|
||||
|
||||
if (this.data.orgDomain.verifiedDate) {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified"));
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.domainNameCtrl.setErrors({
|
||||
errorPassthrough: {
|
||||
message: this.i18nService.t("domainNotVerified", this.domainNameCtrl.value),
|
||||
},
|
||||
});
|
||||
// For the case where user opens dialog and reverifies when domain name formControl disabled.
|
||||
// The input directive only shows error if touched, so must manually mark as touched.
|
||||
this.domainNameCtrl.markAsTouched();
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.updateOrgDomain();
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleVerifyDomainError(e, this.domainNameCtrl.value);
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.updateOrgDomain();
|
||||
}
|
||||
};
|
||||
|
||||
private handleVerifyDomainError(e: any, domainName: string): void {
|
||||
if (e instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = e as ErrorResponse;
|
||||
switch (errorResponse.statusCode) {
|
||||
case HttpStatusCode.Conflict:
|
||||
if (errorResponse.message.includes("The domain is not available to be claimed")) {
|
||||
this.domainNameCtrl.setErrors({
|
||||
errorPassthrough: {
|
||||
message: this.i18nService.t("domainNotAvailable", domainName),
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.validationService.showError(errorResponse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateOrgDomain() {
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.orgDomainApiService.getByOrgIdAndOrgDomainId(
|
||||
this.data.organizationId,
|
||||
this.data.orgDomain.id
|
||||
);
|
||||
}
|
||||
|
||||
deleteDomain = async (): Promise<void> => {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("removeDomainWarning"),
|
||||
this.i18nService.t("removeDomain"),
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.orgDomainApiService.delete(this.data.organizationId, this.data.orgDomain.id);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved"));
|
||||
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
|
||||
export function domainNameValidator(errorMessage: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Domain labels (sections) are only allowed to be 63 chars in length max
|
||||
// 1st and last chars cannot be hyphens per RFC 3696 (https://www.rfc-editor.org/rfc/rfc3696#section-2)
|
||||
// We do not want any prefixes per industry standards.
|
||||
|
||||
// Must support top-level domains and any number of subdomains.
|
||||
// / # start regex
|
||||
// ^ # start of string
|
||||
// (?!(http(s)?:\/\/|www\.)) # negative lookahead to check if input doesn't match "http://", "https://" or "www."
|
||||
// [a-zA-Z0-9] # first character must be a letter or a number
|
||||
// [a-zA-Z0-9-]{0,61} # domain name can have 0 to 61 characters that are letters, numbers, or hyphens
|
||||
// [a-zA-Z0-9] # domain name must end with a letter or a number
|
||||
// (?: # start of non-capturing group (subdomain sections are optional)
|
||||
// \. # subdomain must have a period
|
||||
// [a-zA-Z0-9] # first character of subdomain must be a letter or a number
|
||||
// [a-zA-Z0-9-]{0,61} # subdomain can have 0 to 61 characters that are letters, numbers, or hyphens
|
||||
// [a-zA-Z0-9] # subdomain must end with a letter or a number
|
||||
// )* # end of non-capturing group (subdomain sections are optional)
|
||||
// \. # domain name must have a period
|
||||
// [a-zA-Z]{2,} # domain name must have at least two letters (the domain extension)
|
||||
// $/ # end of string
|
||||
|
||||
const validDomainNameRegex =
|
||||
/^(?!(http(s)?:\/\/|www\.))[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*\.[a-zA-Z]{2,}$/;
|
||||
|
||||
const invalid = !validDomainNameRegex.test(control.value);
|
||||
|
||||
if (invalid) {
|
||||
return {
|
||||
invalidDomainName: {
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||
export function uniqueInArrayValidator(values: Array<string>, errorMessage: string): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lowerTrimmedValue = value.toLowerCase().trim();
|
||||
|
||||
// check if the entered value is unique
|
||||
if (values.some((val) => val.toLowerCase().trim() === lowerTrimmedValue)) {
|
||||
return {
|
||||
nonUniqueValue: {
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<div class="tw-flex tw-flex-row tw-justify-between">
|
||||
<h1>{{ "domainVerification" | i18n }}</h1>
|
||||
|
||||
<button *ngIf="!loading" type="button" buttonType="primary" bitButton (click)="addDomain()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> {{ "newDomain" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
<tr>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "lastChecked" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "options" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let orgDomain of orgDomains; index as i">
|
||||
<td bitCell>
|
||||
<button
|
||||
bitLink
|
||||
type="button"
|
||||
linkType="primary"
|
||||
(click)="editDomain(orgDomain)"
|
||||
appA11yTitle="{{ 'editDomain' | i18n }}"
|
||||
>
|
||||
{{ orgDomain.domainName }}
|
||||
</button>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span *ngIf="!orgDomain?.verifiedDate" bitBadge badgeType="warning">{{
|
||||
"domainStatusUnverified" | i18n
|
||||
}}</span>
|
||||
<span *ngIf="orgDomain?.verifiedDate" bitBadge badgeType="success">{{
|
||||
"domainStatusVerified" | i18n
|
||||
}}</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted">
|
||||
{{ orgDomain.lastCheckedDate | date : "medium" }}
|
||||
</td>
|
||||
|
||||
<td bitCell class="table-list-options tw-text-right">
|
||||
<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 (click)="copyDnsTxt(orgDomain.txt)" type="button">
|
||||
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i>
|
||||
{{ "copyDnsTxtRecord" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitMenuItem
|
||||
(click)="verifyDomain(orgDomain.id, orgDomain.domainName)"
|
||||
type="button"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "verifyDomain" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem (click)="deleteDomain(orgDomain.id)" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
|
||||
<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="" />
|
||||
|
||||
<div class="tw-mb-2 tw-flex tw-flex-row tw-justify-center">
|
||||
<span class="tw-text-lg tw-font-bold">{{ "noDomains" | i18n }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-4 tw-flex tw-flex-row tw-justify-center">
|
||||
<span>
|
||||
{{ "noDomainsSubText" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="button" buttonType="secondary" bitButton (click)="addDomain()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> {{ "newDomain" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { concatMap, Observable, Subject, take, takeUntil } from "rxjs";
|
||||
|
||||
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 { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
DomainAddEditDialogComponent,
|
||||
DomainAddEditDialogData,
|
||||
} from "./domain-add-edit-dialog/domain-add-edit-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-manage-domain-verification",
|
||||
templateUrl: "domain-verification.component.html",
|
||||
})
|
||||
export class DomainVerificationComponent implements OnInit, OnDestroy {
|
||||
private componentDestroyed$ = new Subject<void>();
|
||||
|
||||
loading = true;
|
||||
|
||||
organizationId: string;
|
||||
orgDomains$: Observable<OrganizationDomainResponse[]>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private orgDomainApiService: OrgDomainApiServiceAbstraction,
|
||||
private orgDomainService: OrgDomainServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private validationService: ValidationService
|
||||
) {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
async ngOnInit() {
|
||||
this.orgDomains$ = this.orgDomainService.orgDomains$;
|
||||
|
||||
// Note: going to use concatMap as async subscribe blocks don't work as you expect and
|
||||
// as such, ESLint rejects it
|
||||
// ex: https://stackoverflow.com/a/71056380
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params: Params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
}),
|
||||
takeUntil(this.componentDestroyed$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.orgDomainApiService.getAllByOrgId(this.organizationId);
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addDomain() {
|
||||
const domainAddEditDialogData: DomainAddEditDialogData = {
|
||||
organizationId: this.organizationId,
|
||||
orgDomain: null,
|
||||
existingDomainNames: this.getExistingDomainNames(),
|
||||
};
|
||||
|
||||
this.dialogService.open(DomainAddEditDialogComponent, {
|
||||
data: domainAddEditDialogData,
|
||||
});
|
||||
}
|
||||
|
||||
editDomain(orgDomain: OrganizationDomainResponse) {
|
||||
const domainAddEditDialogData: DomainAddEditDialogData = {
|
||||
organizationId: this.organizationId,
|
||||
orgDomain: orgDomain,
|
||||
existingDomainNames: this.getExistingDomainNames(),
|
||||
};
|
||||
|
||||
this.dialogService.open(DomainAddEditDialogComponent, {
|
||||
data: domainAddEditDialogData,
|
||||
});
|
||||
}
|
||||
|
||||
private getExistingDomainNames(): Array<string> {
|
||||
let existingDomainNames: string[];
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.orgDomains$.pipe(take(1)).subscribe((orgDomains: Array<OrganizationDomainResponse>) => {
|
||||
existingDomainNames = orgDomains.map((o) => o.domainName);
|
||||
});
|
||||
return existingDomainNames;
|
||||
}
|
||||
|
||||
//#region Options
|
||||
|
||||
copyDnsTxt(dnsTxt: string): void {
|
||||
this.orgDomainService.copyDnsTxt(dnsTxt);
|
||||
}
|
||||
|
||||
async verifyDomain(orgDomainId: string, domainName: string): Promise<void> {
|
||||
try {
|
||||
const orgDomain: OrganizationDomainResponse = await this.orgDomainApiService.verify(
|
||||
this.organizationId,
|
||||
orgDomainId
|
||||
);
|
||||
|
||||
if (orgDomain.verifiedDate) {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainVerified"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("domainNotVerified", domainName)
|
||||
);
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.updateOrgDomain(orgDomainId);
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleVerifyDomainError(e, domainName);
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.updateOrgDomain(orgDomainId);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateOrgDomain(orgDomainId: string) {
|
||||
// Update this item so the last checked date gets updated.
|
||||
await this.orgDomainApiService.getByOrgIdAndOrgDomainId(this.organizationId, orgDomainId);
|
||||
}
|
||||
|
||||
private handleVerifyDomainError(e: any, domainName: string): void {
|
||||
if (e instanceof ErrorResponse) {
|
||||
const errorResponse: ErrorResponse = e as ErrorResponse;
|
||||
switch (errorResponse.statusCode) {
|
||||
case HttpStatusCode.Conflict:
|
||||
if (errorResponse.message.includes("The domain is not available to be claimed")) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("domainNotAvailable", domainName)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.validationService.showError(errorResponse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDomain(orgDomainId: string): Promise<void> {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("removeDomainWarning"),
|
||||
this.i18nService.t("removeDomain"),
|
||||
this.i18nService.t("yes"),
|
||||
this.i18nService.t("no"),
|
||||
"warning"
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.orgDomainApiService.delete(this.organizationId, orgDomainId);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainRemoved"));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.componentDestroyed$.next();
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-cons
|
||||
import { SettingsComponent } from "@bitwarden/web-vault/app/admin-console/organizations/settings/settings.component";
|
||||
|
||||
import { SsoComponent } from "../../auth/sso/sso.component";
|
||||
import { DomainVerificationComponent } from "../../organizations/manage/domain-verification/domain-verification.component";
|
||||
|
||||
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
|
||||
import { ScimComponent } from "./manage/scim.component";
|
||||
|
||||
const routes: Routes = [
|
||||
|
||||
@@ -3,10 +3,10 @@ import { NgModule } from "@angular/core";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
|
||||
|
||||
import { SsoComponent } from "../../auth/sso/sso.component";
|
||||
import { DomainAddEditDialogComponent } from "../../organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component";
|
||||
import { DomainVerificationComponent } from "../../organizations/manage/domain-verification/domain-verification.component";
|
||||
|
||||
import { InputCheckboxComponent } from "./components/input-checkbox.component";
|
||||
import { DomainAddEditDialogComponent } from "./manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component";
|
||||
import { DomainVerificationComponent } from "./manage/domain-verification/domain-verification.component";
|
||||
import { ScimComponent } from "./manage/scim.component";
|
||||
import { OrganizationsRoutingModule } from "./organizations-routing.module";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user