mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
add routes and ui for user disabledInteractionsUris state management
This commit is contained in:
@@ -2324,6 +2324,9 @@
|
|||||||
"message": "Domains",
|
"message": "Domains",
|
||||||
"description": "A category title describing the concept of web domains"
|
"description": "A category title describing the concept of web domains"
|
||||||
},
|
},
|
||||||
|
"disabledDomains": {
|
||||||
|
"message": "Disabled domains"
|
||||||
|
},
|
||||||
"excludedDomains": {
|
"excludedDomains": {
|
||||||
"message": "Excluded domains"
|
"message": "Excluded domains"
|
||||||
},
|
},
|
||||||
@@ -2333,6 +2336,12 @@
|
|||||||
"excludedDomainsDescAlt": {
|
"excludedDomainsDescAlt": {
|
||||||
"message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect."
|
"message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect."
|
||||||
},
|
},
|
||||||
|
"disabledDomainsDesc": {
|
||||||
|
"message": "Bitwarden will be disabled for these domains. You must refresh the page for changes to take effect."
|
||||||
|
},
|
||||||
|
"disabledDomainsDescAlt": {
|
||||||
|
"message": "Bitwarden will be disabled for these domains on all logged in accounts. You must refresh the page for changes to take effect."
|
||||||
|
},
|
||||||
"websiteItemLabel": {
|
"websiteItemLabel": {
|
||||||
"message": "Website $number$ (URI)",
|
"message": "Website $number$ (URI)",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -2351,6 +2360,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"disabledDomainsSavedSuccess": {
|
||||||
|
"message": "Excluded domain changes saved"
|
||||||
|
},
|
||||||
"excludedDomainsSavedSuccess": {
|
"excludedDomainsSavedSuccess": {
|
||||||
"message": "Excluded domain changes saved"
|
"message": "Excluded domain changes saved"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -135,6 +135,14 @@
|
|||||||
<div class="row-main">{{ "logOut" | i18n }}</div>
|
<div class="row-main">{{ "logOut" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/disabled-domains"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "disabledDomains" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -118,6 +118,10 @@
|
|||||||
<button bit-item-content type="button" appStopClick (click)="logOut()"></button>
|
<button bit-item-content type="button" appStopClick (click)="logOut()"></button>
|
||||||
{{ "logOut" | i18n }}
|
{{ "logOut" | i18n }}
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/disabled-domains">{{ "disabledDomains" | i18n }}</a>
|
||||||
|
<i slot="end" class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</bit-item>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
</div>
|
</div>
|
||||||
</popup-page>
|
</popup-page>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="{{ 'disabledDomains' | i18n }}" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
<div class="tw-bg-background-alt">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
accountSwitcherEnabled ? ("disabledDomainsDescAlt" | i18n) : ("disabledDomainsDesc" | i18n)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<bit-section *ngIf="!isLoading">
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2>
|
||||||
|
<span bitTypography="body2" slot="end">{{ disabledDomainsState?.length || 0 }}</span>
|
||||||
|
</bit-section-header>
|
||||||
|
|
||||||
|
<ng-container *ngIf="disabledDomainsState">
|
||||||
|
<bit-item
|
||||||
|
*ngFor="let domain of disabledDomainsState; let i = index; trackBy: trackByFunction"
|
||||||
|
>
|
||||||
|
<bit-item-content>
|
||||||
|
<bit-label *ngIf="i >= fieldsEditThreshold">{{
|
||||||
|
"websiteItemLabel" | i18n: i + 1
|
||||||
|
}}</bit-label>
|
||||||
|
<input
|
||||||
|
*ngIf="i >= fieldsEditThreshold"
|
||||||
|
#uriInput
|
||||||
|
appInputVerbatim
|
||||||
|
bitInput
|
||||||
|
id="excludedDomain{{ i }}"
|
||||||
|
inputmode="url"
|
||||||
|
name="excludedDomain{{ i }}"
|
||||||
|
type="text"
|
||||||
|
(change)="fieldChange()"
|
||||||
|
[(ngModel)]="disabledDomainsState[i]"
|
||||||
|
/>
|
||||||
|
<div id="excludedDomain{{ i }}" *ngIf="i < fieldsEditThreshold">{{ domain }}</div>
|
||||||
|
</bit-item-content>
|
||||||
|
<button
|
||||||
|
*ngIf="i < fieldsEditThreshold"
|
||||||
|
appA11yTitle="{{ 'remove' | i18n }}"
|
||||||
|
bitIconButton="bwi-minus-circle"
|
||||||
|
buttonType="danger"
|
||||||
|
size="small"
|
||||||
|
slot="end"
|
||||||
|
type="button"
|
||||||
|
(click)="removeDomain(i)"
|
||||||
|
></button>
|
||||||
|
</bit-item>
|
||||||
|
</ng-container>
|
||||||
|
<button bitLink class="tw-pt-2" type="button" linkType="primary" (click)="addNewDomain()">
|
||||||
|
<i class="bwi bwi-plus-circle bwi-fw" aria-hidden="true"></i> {{ "addDomain" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-section>
|
||||||
|
</div>
|
||||||
|
<popup-footer slot="footer">
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
[disabled]="dataIsPristine"
|
||||||
|
(click)="saveChanges()"
|
||||||
|
>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</popup-footer>
|
||||||
|
</popup-page>
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import {
|
||||||
|
QueryList,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
AfterViewInit,
|
||||||
|
ViewChildren,
|
||||||
|
} from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
CardComponent,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
LinkModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-disabled-domains",
|
||||||
|
templateUrl: "disabled-domains.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
CardComponent,
|
||||||
|
CommonModule,
|
||||||
|
FormFieldModule,
|
||||||
|
FormsModule,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
JslibModule,
|
||||||
|
LinkModule,
|
||||||
|
PopOutComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
RouterModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DisabledDomainsComponent implements AfterViewInit, OnDestroy {
|
||||||
|
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>>;
|
||||||
|
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
dataIsPristine = true;
|
||||||
|
isLoading = false;
|
||||||
|
disabledDomainsState: string[] = [];
|
||||||
|
storedDisabledDomains: string[] = [];
|
||||||
|
// How many fields should be non-editable before editable fields
|
||||||
|
fieldsEditThreshold: number = 0;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngAfterViewInit() {
|
||||||
|
this.domainSettingsService.disabledInteractionsUris$
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((neverDomains: NeverDomains) => this.handleStateUpdate(neverDomains));
|
||||||
|
|
||||||
|
this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
|
||||||
|
this.focusNewUriInput(last);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStateUpdate(neverDomains: NeverDomains) {
|
||||||
|
if (neverDomains) {
|
||||||
|
this.storedDisabledDomains = Object.keys(neverDomains);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disabledDomainsState = [...this.storedDisabledDomains];
|
||||||
|
|
||||||
|
// Do not allow the first x (pre-existing) fields to be edited
|
||||||
|
this.fieldsEditThreshold = this.storedDisabledDomains.length;
|
||||||
|
|
||||||
|
this.dataIsPristine = true;
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
focusNewUriInput(elementRef: ElementRef) {
|
||||||
|
if (elementRef?.nativeElement) {
|
||||||
|
elementRef.nativeElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addNewDomain() {
|
||||||
|
// add empty field to the Domains list interface
|
||||||
|
this.disabledDomainsState.push("");
|
||||||
|
|
||||||
|
await this.fieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeDomain(i: number) {
|
||||||
|
this.disabledDomainsState.splice(i, 1);
|
||||||
|
|
||||||
|
// If a pre-existing field was dropped, lower the edit threshold
|
||||||
|
if (i < this.fieldsEditThreshold) {
|
||||||
|
this.fieldsEditThreshold--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fieldChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fieldChange() {
|
||||||
|
if (this.dataIsPristine) {
|
||||||
|
this.dataIsPristine = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveChanges() {
|
||||||
|
if (this.dataIsPristine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
const newDisabledDomainsSaveState: NeverDomains = {};
|
||||||
|
const uniqueDisabledDomains = new Set(this.disabledDomainsState);
|
||||||
|
|
||||||
|
for (const uri of uniqueDisabledDomains) {
|
||||||
|
if (uri && uri !== "") {
|
||||||
|
const validatedHost = Utils.getHostname(uri);
|
||||||
|
|
||||||
|
if (!validatedHost) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("excludedDomainsInvalidDomain", uri),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't reset via `handleStateUpdate` to allow existing input value correction
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newDisabledDomainsSaveState[validatedHost] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingState = new Set(this.storedDisabledDomains);
|
||||||
|
const newState = new Set(Object.keys(newDisabledDomainsSaveState));
|
||||||
|
const stateIsUnchanged =
|
||||||
|
existingState.size === newState.size &&
|
||||||
|
new Set([...existingState, ...newState]).size === existingState.size;
|
||||||
|
|
||||||
|
// The subscriber updates don't trigger if `setNeverDomains` sets an equivalent state
|
||||||
|
if (stateIsUnchanged) {
|
||||||
|
// Reset UI state directly
|
||||||
|
const constructedNeverDomainsState = this.storedDisabledDomains.reduce(
|
||||||
|
(neverDomains, uri) => ({ ...neverDomains, [uri]: null }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
this.handleStateUpdate(constructedNeverDomainsState);
|
||||||
|
} else {
|
||||||
|
await this.domainSettingsService.setDisabledInteractionsUris(newDisabledDomainsSaveState);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("disabledDomainsSavedSuccess"),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||||
|
|
||||||
|
// Don't reset via `handleStateUpdate` to preserve input values
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByFunction(index: number, _: string) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@ import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
|
|||||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||||
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { DisabledDomainsComponent } from "../autofill/popup/settings/disabled-domains.component";
|
||||||
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
|
||||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
|
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
|
||||||
@@ -389,6 +390,12 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "disabled-domains",
|
||||||
|
component: DisabledDomainsComponent,
|
||||||
|
canActivate: [authGuard],
|
||||||
|
data: { state: "disabled-domains" } satisfies RouteDataProperties,
|
||||||
|
},
|
||||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||||
path: "excluded-domains",
|
path: "excluded-domains",
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
|||||||
Reference in New Issue
Block a user