1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-27 22:11:38 +01:00

[PM-16470] Remove v1 autofill owned refresh code (#12579)

* Remove v1 autofill owned settings

* Remove Fido2 v1 components

* Remove conditional to enable AutofillOnPageLoadOrgPolicy

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith 2025-01-13 14:44:42 +01:00 committed by GitHub
parent fe9b0976ee
commit 22f4822efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 14 additions and 1860 deletions

View File

@ -1,36 +0,0 @@
<div
role="group"
appA11yTitle="{{ cipher.name }} {{ cipher.subTitle }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
tabindex="0"
appStopClick
title="{{ title }} - {{ cipher.name }}"
[ngClass]="{ 'row-main': true, 'row-selected': isSelected && !isSearching }"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
<ng-container *ngIf="cipher.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="detail" *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</span>
<span class="detail" *ngIf="cipher.subTitle">{{ cipher.subTitle }}</span>
</div>
</button>
</div>
</div>

View File

@ -1,41 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
selector: "app-fido2-cipher-row-v1",
templateUrl: "fido2-cipher-row-v1.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Fido2CipherRowV1Component {
@Output() onSelected = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() title: string;
@Input() isSearching: boolean;
@Input() isSelected: boolean;
protected selectCipher(c: CipherView) {
this.onSelected.emit(c);
}
/**
* Returns a subname for the cipher.
* If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId.
* @param c Cipher
* @returns
*/
protected getSubName(c: CipherView): string | null {
const fido2Credentials = c.login?.fido2Credentials;
if (!fido2Credentials || fido2Credentials.length === 0) {
return null;
}
const [fido2Credential] = fido2Credentials;
return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null;
}
}

View File

@ -1,52 +0,0 @@
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
<div class="useBrowserlink">
<button
type="button"
(click)="toggle()"
cdkOverlayOrigin
#trigger="cdkOverlayOrigin"
aria-haspopup="dialog"
aria-controls="cdk-overlay-container"
>
<span class="text-primary">
{{ "useDeviceOrHardwareKey" | i18n }}
</span>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</button>
</div>
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen"
[cdkConnectedOverlayPositions]="overlayPosition"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
(backdropClick)="isOpen = false"
(detach)="close()"
>
<div class="box-content">
<div
class="fido2-browser-selector-dropdown"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
<span>{{ "justOnce" | i18n }}</span>
</button>
<br />
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
<span>{{ "alwaysForThisSite" | i18n }}</span>
</button>
</div>
</div>
</ng-template>
<div
*ngIf="showOverlay"
class="tw-absolute tw-w-full tw-h-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
></div>
</ng-container>

View File

@ -1,115 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
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 { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
@Component({
selector: "app-fido2-use-browser-link-v1",
templateUrl: "fido2-use-browser-link-v1.component.html",
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
}),
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
}),
),
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
})
export class Fido2UseBrowserLinkV1Component {
showOverlay = false;
isOpen = false;
overlayPosition: ConnectedPosition[] = [
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
offsetY: 5,
},
];
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {}
toggle() {
this.isOpen = !this.isOpen;
}
close() {
this.isOpen = false;
}
/**
* Aborts the current FIDO2 session and fallsback to the browser.
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
*/
protected async abort(excludeDomain = true) {
this.close();
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (!excludeDomain) {
this.abortSession(sessionData.sessionId);
return;
}
// Show overlay to prevent the user from interacting with the page.
this.showOverlay = true;
await this.handleDomainExclusion(sessionData.senderUrl);
// Give the user a chance to see the toast before closing the popout.
await Utils.delay(2000);
this.abortSession(sessionData.sessionId);
}
/**
* Excludes the domain from future FIDO2 prompts.
* @param uri - The domain uri to exclude from future FIDO2 prompts.
*/
private async handleDomainExclusion(uri: string) {
const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri);
const savedDomains: NeverDomains = {
...existingDomains,
};
savedDomains[validDomain] = null;
await this.domainSettingsService.setNeverDomains(savedDomains);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("domainAddedToExcludedDomains", validDomain),
);
}
private abortSession(sessionId: string) {
BrowserFido2UserInterfaceSession.abortPopout(sessionId, true);
}
}

View File

@ -1,142 +0,0 @@
<ng-container *ngIf="data$ | async as data">
<div class="auth-wrapper">
<div class="auth-header">
<div class="left">
<ng-container *ngIf="data.message.type != BrowserFido2MessageTypes.PickCredentialRequest">
<div class="logo">
<i class="bwi bwi-shield"></i>
</div>
</ng-container>
<ng-container *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
<div class="logo">
<i class="bwi bwi-shield"></i><span><strong>bit</strong>warden</span>
</div>
</ng-container>
</div>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest"
>
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search()"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</ng-container>
</div>
<ng-container>
<ng-container
*ngIf="
data.message.type === BrowserFido2MessageTypes.PickCredentialRequest ||
data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest
"
>
<div class="auth-flow">
<p class="subtitle" appA11yTitle="{{ subtitleText | i18n }}">
{{ subtitleText | i18n }}
</p>
<!-- Display when ciphers exist -->
<ng-container *ngIf="displayedCiphers.length > 0">
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row-v1
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
[isSearching]="searchPending"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row-v1>
</div>
</div>
<div class="box">
<button
type="submit"
(click)="submit()"
class="btn primary block"
appA11yTitle="{{ credentialText | i18n }}"
>
<span [hidden]="loading">
{{ credentialText | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
<ng-container *ngIf="!displayedCiphers.length">
<div class="box">
<button
type="submit"
(click)="saveNewLogin()"
class="btn primary block"
appA11yTitle="{{ 'savePasskeyNewLogin' | i18n }}"
>
<span [hidden]="loading">
{{ "savePasskeyNewLogin" | i18n }}
</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!loading"
aria-hidden="true"
></i>
</button>
</div>
</ng-container>
</div>
</ng-container>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
<div class="box list">
<div class="box-content">
<app-fido2-cipher-row-v1
*ngFor="let cipherItem of displayedCiphers"
[cipher]="cipherItem"
title="{{ 'passkeyItem' | i18n }}"
(onSelected)="selectedPasskey($event)"
[isSelected]="cipher === cipherItem"
></app-fido2-cipher-row-v1>
</div>
</div>
<button type="button" class="btn primary block" (click)="viewPasskey()">
<span [hidden]="loading">{{ "viewItem" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</div>
</ng-container>
<ng-container
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
>
<div class="auth-flow">
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
</div>
<button type="button" class="btn primary block" (click)="abort(false)">
<span [hidden]="loading">{{ "close" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
</button>
</ng-container>
</ng-container>
<app-fido2-use-browser-link-v1></app-fido2-use-browser-link-v1>
</div>
</ng-container>

View File

@ -1,445 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
concatMap,
filter,
firstValueFrom,
map,
Observable,
Subject,
take,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service";
import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window";
import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service";
import {
BrowserFido2Message,
BrowserFido2UserInterfaceSession,
BrowserFido2MessageTypes,
} from "../../fido2/services/browser-fido2-user-interface.service";
interface ViewData {
message: BrowserFido2Message;
fallbackSupported: boolean;
}
@Component({
selector: "app-fido2-v1",
templateUrl: "fido2-v1.component.html",
styleUrls: [],
})
export class Fido2V1Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private hasSearched = false;
protected cipher: CipherView;
protected searchTypeSearch = false;
protected searchPending = false;
protected searchText: string;
protected url: string;
protected hostname: string;
protected data$: Observable<ViewData>;
protected sessionId?: string;
protected senderTabId?: string;
protected ciphers?: CipherView[] = [];
protected displayedCiphers?: CipherView[] = [];
protected loading = false;
protected subtitleText: string;
protected credentialText: string;
protected BrowserFido2MessageTypes = BrowserFido2MessageTypes;
private message$ = new BehaviorSubject<BrowserFido2Message>(null);
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
private cipherService: CipherService,
private platformUtilsService: PlatformUtilsService,
private domainSettingsService: DomainSettingsService,
private searchService: SearchService,
private logService: LogService,
private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService,
private passwordRepromptService: PasswordRepromptService,
private fido2UserVerificationService: Fido2UserVerificationService,
private accountService: AccountService,
) {}
ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
const queryParams$ = this.activatedRoute.queryParamMap.pipe(
take(1),
map((queryParamMap) => ({
sessionId: queryParamMap.get("sessionId"),
senderTabId: queryParamMap.get("senderTabId"),
senderUrl: queryParamMap.get("senderUrl"),
})),
);
combineLatest([
queryParams$,
this.browserMessagingApi.messageListener$() as Observable<BrowserFido2Message>,
])
.pipe(
concatMap(async ([queryParams, message]) => {
this.sessionId = queryParams.sessionId;
this.senderTabId = queryParams.senderTabId;
this.url = queryParams.senderUrl;
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
if (
message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest &&
message.sessionId !== queryParams.sessionId
) {
this.abort(false);
return;
}
// Ignore messages that don't belong to the current session.
if (message.sessionId !== queryParams.sessionId) {
return;
}
if (message.type === BrowserFido2MessageTypes.AbortRequest) {
this.abort(false);
return;
}
return message;
}),
filter((message) => !!message),
takeUntil(this.destroy$),
)
.subscribe((message) => {
this.message$.next(message);
});
this.data$ = this.message$.pipe(
filter((message) => message != undefined),
concatMap(async (message) => {
switch (message.type) {
case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
);
this.displayedCiphers = this.ciphers.filter(
(cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains) &&
this.hasNoOtherPasskeys(cipher, message.userHandle),
);
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case BrowserFido2MessageTypes.PickCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
case BrowserFido2MessageTypes.InformExcludedCredentialRequest: {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}),
);
this.displayedCiphers = [...this.ciphers];
if (this.displayedCiphers.length > 0) {
this.selectedPasskey(this.displayedCiphers[0]);
}
break;
}
}
this.subtitleText =
this.displayedCiphers.length > 0
? this.getCredentialSubTitleText(message.type)
: "noMatchingPasskeyLogin";
this.credentialText = this.getCredentialButtonText(message.type);
return {
message,
fallbackSupported: "fallbackSupported" in message && message.fallbackSupported,
};
}),
takeUntil(this.destroy$),
);
queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => {
this.send({
sessionId: queryParams.sessionId,
type: BrowserFido2MessageTypes.ConnectResponse,
});
});
}
protected async submit() {
const data = this.message$.value;
if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) {
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: BrowserFido2MessageTypes.PickCredentialResponse,
userVerified,
});
} else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
if (this.cipher.login.hasFido2Credentials) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "overwritePasskey" },
content: { key: "overwritePasskeyAlert" },
type: "info",
});
if (!confirmed) {
return false;
}
}
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
this.send({
sessionId: this.sessionId,
cipherId: this.cipher.id,
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified,
});
}
this.loading = true;
}
protected async saveNewLogin() {
const data = this.message$.value;
if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
const name = data.credentialName || data.rpId;
// TODO: Revert to check for user verification once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
await this.createNewCipher(name, data.userName);
// We are bypassing user verification pending approval.
this.send({
sessionId: this.sessionId,
cipherId: this.cipher?.id,
type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse,
userVerified: data.userVerification,
});
}
this.loading = true;
}
getCredentialSubTitleText(messageType: string): string {
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
? "chooseCipherForPasskeySave"
: "logInWithPasskeyQuestion";
}
getCredentialButtonText(messageType: string): string {
return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest
? "savePasskey"
: "confirm";
}
selectedPasskey(item: CipherView) {
this.cipher = item;
}
viewPasskey() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: {
cipherId: this.cipher.id,
uilocation: "popout",
senderTabId: this.senderTabId,
sessionId: this.sessionId,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
addCipher() {
const data = this.message$.value;
if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
name: data.credentialName || data.rpId,
uri: this.url,
type: CipherType.Login.toString(),
uilocation: "popout",
username: data.userName,
senderTabId: this.senderTabId,
sessionId: this.sessionId,
userVerification: data.userVerification,
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
},
});
}
protected async search() {
this.hasSearched = await this.searchService.isSearchable(this.searchText);
this.searchPending = true;
if (this.hasSearched) {
this.displayedCiphers = await this.searchService.searchCiphers(
this.searchText,
null,
this.ciphers,
);
} else {
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains),
);
}
this.searchPending = false;
this.selectedPasskey(this.displayedCiphers[0]);
}
abort(fallback: boolean) {
this.unload(fallback);
window.close();
}
unload(fallback = false) {
this.send({
sessionId: this.sessionId,
type: BrowserFido2MessageTypes.AbortResponse,
fallbackRequested: fallback,
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private buildCipher(name: string, username: string) {
this.cipher = new CipherView();
this.cipher.name = name;
this.cipher.type = CipherType.Login;
this.cipher.login = new LoginView();
this.cipher.login.username = username;
this.cipher.login.uris = [new LoginUriView()];
this.cipher.login.uris[0].uri = this.url;
this.cipher.card = new CardView();
this.cipher.identity = new IdentityView();
this.cipher.secureNote = new SecureNoteView();
this.cipher.secureNote.type = SecureNoteType.Generic;
this.cipher.reprompt = CipherRepromptType.None;
}
private async createNewCipher(name: string, username: string) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.buildCipher(name, username);
const cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
try {
await this.cipherService.createWithServer(cipher);
this.cipher.id = cipher.id;
} catch (e) {
this.logService.error(e);
}
}
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
private async handleUserVerification(
userVerificationRequested: boolean,
cipher: CipherView,
): Promise<boolean> {
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
if (masterPasswordRepromptRequired) {
return await this.passwordRepromptService.showPasswordPrompt();
}
return userVerificationRequested;
}
private send(msg: BrowserFido2Message) {
BrowserFido2UserInterfaceSession.sendMessage({
sessionId: this.sessionId,
...msg,
});
}
/**
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
* @param userHandle
*/
private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean {
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
return true;
}
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
}
}

View File

@ -1,270 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "autofill" | i18n }}</span>
</h1>
<div class="right"></div>
</header>
<main tabindex="-1">
<div class="box tw-mt-4">
<div class="box-content">
<button
type="button"
class="box-content-row box-content-row-link box-content-row-flex"
(click)="commandSettings()"
>
<div class="row-main">{{ "autofillShortcut" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
<div id="autofillKeyboardHelp" class="box-footer">
{{ autofillKeyboardHelperText }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="autofill-overlay-settings">{{ "showAutoFillMenuOnFormFields" | i18n }}</label>
<select
id="autofill-overlay-settings"
name="autofill-overlay-settings"
[(ngModel)]="autoFillOverlayVisibility"
(change)="updateAutoFillOverlayVisibility()"
>
<option *ngFor="let o of autoFillOverlayVisibilityOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div class="box-footer" *ngIf="accountSwitcherEnabled || !canOverrideBrowserAutofillSetting">
<span *ngIf="accountSwitcherEnabled">{{ "showInlineMenuOnFormFieldsDescAlt" | i18n }}</span>
<span *ngIf="!canOverrideBrowserAutofillSetting">
{{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }}
<a
[attr.href]="disablePasswordManagerLink"
(click)="openDisablePasswordManagerLink($event)"
target="_blank"
rel="noreferrer"
>
{{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }}
</a>
</span>
</div>
</div>
</div>
<div class="box tw-mb-5" *ngIf="inlineMenuPositioningImprovementsEnabled && inlineMenuIsEnabled">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="show-inline-menu-identities" class="!tw-mr-0">{{
"showInlineMenuIdentitiesLabel" | i18n
}}</label>
<input
id="show-inline-menu-identities"
type="checkbox"
(change)="updateShowInlineMenuIdentities()"
[(ngModel)]="showInlineMenuIdentities"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="show-inline-menu-cards" class="!tw-mr-0">{{
"showInlineMenuCardsLabel" | i18n
}}</label>
<input
id="show-inline-menu-cards"
type="checkbox"
(change)="updateShowInlineMenuCards()"
[(ngModel)]="showInlineMenuCards"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-content" *ngIf="canOverrideBrowserAutofillSetting">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="overrideBrowserAutofill" class="!tw-mr-0">{{
"overrideDefaultBrowserAutoFillSettings" | i18n
}}</label>
<input
id="overrideBrowserAutofill"
type="checkbox"
(change)="updateDefaultBrowserAutofillDisabled()"
[(ngModel)]="defaultBrowserAutofillDisabled"
/>
</div>
</div>
<div class="box-footer" *ngIf="canOverrideBrowserAutofillSetting">
<span *ngIf="accountSwitcherEnabled">{{ "showInlineMenuOnFormFieldsDescAlt" | i18n }}</span>
{{ "turnOffBrowserBuiltInPasswordManagerSettings" | i18n }}
<a
[attr.href]="disablePasswordManagerLink"
(click)="openDisablePasswordManagerLink($event)"
target="_blank"
rel="noreferrer"
>
{{ "turnOffBrowserBuiltInPasswordManagerSettingsLink" | i18n }}
</a>
</div>
</div>
<div class="box tw-mt-4">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="autofill">{{ "enableAutoFillOnPageLoad" | i18n }}</label>
<input
id="autofill"
type="checkbox"
aria-describedby="autofillHelp"
(change)="updateAutoFillOnPageLoad()"
[(ngModel)]="enableAutoFillOnPageLoad"
/>
</div>
</div>
<div id="autofillHelp" class="box-footer">
{{ "enableAutoFillOnPageLoadDesc" | i18n }}
<b>{{ "warning" | i18n }}</b
>: {{ "experimentalFeature" | i18n }}
<a href="https://bitwarden.com/help/auto-fill-browser/" target="_blank" rel="noreferrer">
{{ "learnMoreAboutAutofill" | i18n }}.
<i
[attr.aria-label]="'opensInANewWindow' | i18n"
class="bwi bwi-external-link bwi-sm bwi-fw"
></i>
</a>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="defaultAutofill">{{ "defaultAutoFillOnPageLoad" | i18n }}</label>
<select
id="defaultAutofill"
name="DefaultAutofill"
aria-describedby="defaultAutofillHelp"
[(ngModel)]="autoFillOnPageLoadDefault"
(change)="updateAutoFillOnPageLoadDefault()"
[disabled]="!enableAutoFillOnPageLoad"
>
<option *ngFor="let o of autoFillOnPageLoadOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
</div>
<div id="defaultAutofillHelp" class="box-footer">
{{ "defaultAutoFillOnPageLoadDesc" | i18n }}
</div>
</div>
<div class="box">
<h2 class="box-header">{{ "additionalOptions" | i18n }}</h2>
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="context-menu">{{ "enableContextMenuItem" | i18n }}</label>
<input
id="context-menu"
type="checkbox"
aria-describedby="context-menuHelp"
(change)="updateContextMenuItem()"
[(ngModel)]="enableContextMenuItem"
/>
</div>
</div>
<div id="context-menuHelp" class="box-footer">
{{
accountSwitcherEnabled ? ("contextMenuItemDescAlt" | i18n) : ("contextMenuItemDesc" | i18n)
}}
</div>
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="totp">{{ "enableAutoTotpCopy" | i18n }}</label>
<input
id="totp"
type="checkbox"
aria-describedby="totpHelp"
(change)="updateAutoTotpCopy()"
[(ngModel)]="enableAutoTotpCopy"
/>
</div>
</div>
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="clearClipboard">{{ "clearClipboard" | i18n }}</label>
<select
id="clearClipboard"
name="ClearClipboard"
aria-describedby="clearClipboardHelp"
[(ngModel)]="clearClipboard"
(change)="saveClearClipboard()"
>
<option *ngFor="let o of clearClipboardOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
</div>
<div id="clearClipboardHelp" class="box-footer">{{ "clearClipboardDesc" | i18n }}</div>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="defaultUriMatch">{{ "defaultUriMatchDetection" | i18n }}</label>
<select
id="defaultUriMatch"
name="DefaultUriMatch"
aria-describedby="defaultUriMatchHelp"
[(ngModel)]="defaultUriMatch"
(change)="saveDefaultUriMatch()"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="defaultUriMatchHelp" class="box-footer">
{{ "defaultUriMatchDetectionDesc" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="showCardsCurrentTab">{{ "showCardsCurrentTab" | i18n }}</label>
<input
id="showCardsCurrentTab"
type="checkbox"
aria-describedby="showCardsCurrentTabHelp"
(change)="updateShowCardsCurrentTab()"
[(ngModel)]="showCardsCurrentTab"
/>
</div>
</div>
<div id="showCardsCurrentTabHelp" class="box-footer">
{{ "showCardsCurrentTabDesc" | i18n }}
</div>
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="showIdentitiesCurrentTab">{{ "showIdentitiesCurrentTab" | i18n }}</label>
<input
id="showIdentitiesCurrentTab"
type="checkbox"
aria-describedby="showIdentitiesCurrentTabHelp"
(change)="updateShowIdentitiesCurrentTab()"
[(ngModel)]="showIdentitiesCurrentTab"
/>
</div>
</div>
<div id="showIdentitiesCurrentTabHelp" class="box-footer">
{{ "showIdentitiesCurrentTabDesc" | i18n }}
</div>
</div>
<div class="box list" *ngIf="blockBrowserInjectionsByDomainEnabled">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/blocked-domains"
>
<div class="row-main">{{ "blockedDomains" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@ -1,344 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import {
InlineMenuVisibilitySetting,
ClearClipboardDelaySetting,
} from "@bitwarden/common/autofill/types";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "app-autofill-v1",
templateUrl: "autofill-v1.component.html",
})
export class AutofillV1Component implements OnInit {
protected canOverrideBrowserAutofillSetting = false;
protected defaultBrowserAutofillDisabled = false;
protected autoFillOverlayVisibility: InlineMenuVisibilitySetting;
protected autoFillOverlayVisibilityOptions: any[];
protected disablePasswordManagerLink: string;
protected inlineMenuPositioningImprovementsEnabled: boolean = false;
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
protected showInlineMenuIdentities: boolean = true;
protected showInlineMenuCards: boolean = true;
inlineMenuIsEnabled: boolean = false;
enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[];
enableContextMenuItem = false;
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
clearClipboard: ClearClipboardDelaySetting;
clearClipboardOptions: any[];
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[];
showCardsCurrentTab = false;
showIdentitiesCurrentTab = false;
autofillKeyboardHelperText: string;
accountSwitcherEnabled = false;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private domainSettingsService: DomainSettingsService,
private configService: ConfigService,
private dialogService: DialogService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private messagingService: MessagingService,
private vaultSettingsService: VaultSettingsService,
) {
this.autoFillOverlayVisibilityOptions = [
{
name: i18nService.t("autofillOverlayVisibilityOff"),
value: AutofillOverlayVisibility.Off,
},
{
name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"),
value: AutofillOverlayVisibility.OnFieldFocus,
},
{
name: i18nService.t("autofillOverlayVisibilityOnButtonClick"),
value: AutofillOverlayVisibility.OnButtonClick,
},
];
this.autoFillOnPageLoadOptions = [
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
];
this.clearClipboardOptions = [
{ name: i18nService.t("never"), value: null },
{ name: i18nService.t("tenSeconds"), value: 10 },
{ name: i18nService.t("twentySeconds"), value: 20 },
{ name: i18nService.t("thirtySeconds"), value: 30 },
{ name: i18nService.t("oneMinute"), value: 60 },
{ name: i18nService.t("twoMinutes"), value: 120 },
{ name: i18nService.t("fiveMinutes"), value: 300 },
];
this.uriMatchOptions = [
{ name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
];
this.accountSwitcherEnabled = enableAccountSwitching();
this.disablePasswordManagerLink = this.getDisablePasswordManagerLink();
}
async ngOnInit() {
this.canOverrideBrowserAutofillSetting =
this.platformUtilsService.isChrome() ||
this.platformUtilsService.isEdge() ||
this.platformUtilsService.isOpera() ||
this.platformUtilsService.isVivaldi();
this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden();
this.autoFillOverlayVisibility = await firstValueFrom(
this.autofillSettingsService.inlineMenuVisibility$,
);
this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.InlineMenuPositioningImprovements,
);
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
FeatureFlag.BlockBrowserInjectionsByDomain,
);
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
this.showInlineMenuIdentities =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$));
this.showInlineMenuCards =
this.inlineMenuPositioningImprovementsEnabled &&
(await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$));
this.enableAutoFillOnPageLoad = await firstValueFrom(
this.autofillSettingsService.autofillOnPageLoad$,
);
this.autoFillOnPageLoadDefault = await firstValueFrom(
this.autofillSettingsService.autofillOnPageLoadDefault$,
);
this.enableContextMenuItem = await firstValueFrom(
this.autofillSettingsService.enableContextMenu$,
);
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
const defaultUriMatch = await firstValueFrom(
this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillKeyboardHelperText(command);
this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$);
this.showIdentitiesCurrentTab = await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
);
}
isInlineMenuEnabled() {
return (
this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus ||
this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick
);
}
async updateAutoFillOverlayVisibility() {
await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility);
await this.requestPrivacyPermission();
this.inlineMenuIsEnabled = this.isInlineMenuEnabled();
}
async updateAutoFillOnPageLoad() {
await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad);
}
async updateAutoFillOnPageLoadDefault() {
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
}
async saveDefaultUriMatch() {
await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch);
}
private async setAutofillKeyboardHelperText(command: string) {
if (command) {
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command);
} else {
this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet");
}
}
async commandSettings() {
if (this.platformUtilsService.isChrome()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("chrome://extensions/shortcuts");
} else if (this.platformUtilsService.isOpera()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("opera://extensions/shortcuts");
} else if (this.platformUtilsService.isEdge()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("edge://extensions/shortcuts");
} else if (this.platformUtilsService.isVivaldi()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("vivaldi://extensions/shortcuts");
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("https://bitwarden.com/help/keyboard-shortcuts");
}
}
private getDisablePasswordManagerLink(): string {
if (this.platformUtilsService.isChrome()) {
return "chrome://settings/autofill";
}
if (this.platformUtilsService.isOpera()) {
return "opera://settings/autofill";
}
if (this.platformUtilsService.isEdge()) {
return "edge://settings/passwords";
}
if (this.platformUtilsService.isVivaldi()) {
return "vivaldi://settings/autofill";
}
return "https://bitwarden.com/help/disable-browser-autofill/";
}
protected openDisablePasswordManagerLink(event: Event) {
event.preventDefault();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab(this.disablePasswordManagerLink);
}
async requestPrivacyPermission() {
if (
this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off ||
!this.canOverrideBrowserAutofillSetting ||
(await this.browserAutofillSettingCurrentlyOverridden())
) {
return;
}
await this.dialogService.openSimpleDialog({
title: { key: "overrideDefaultBrowserAutofillTitle" },
content: { key: "overrideDefaultBrowserAutofillDescription" },
acceptButtonText: { key: "makeDefault" },
acceptAction: async () => await this.handleOverrideDialogAccept(),
cancelButtonText: { key: "ignore" },
type: "info",
});
}
async updateDefaultBrowserAutofillDisabled() {
const privacyPermissionGranted = await this.privacyPermissionGranted();
if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) {
return;
}
if (
!privacyPermissionGranted &&
!(await BrowserApi.requestPermission({ permissions: ["privacy"] }))
) {
await this.dialogService.openSimpleDialog({
title: { key: "privacyPermissionAdditionNotGrantedTitle" },
content: { key: "privacyPermissionAdditionNotGrantedDescription" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "warning",
});
this.defaultBrowserAutofillDisabled = false;
return;
}
BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
}
private handleOverrideDialogAccept = async () => {
this.defaultBrowserAutofillDisabled = true;
await this.updateDefaultBrowserAutofillDisabled();
};
async browserAutofillSettingCurrentlyOverridden() {
if (!this.canOverrideBrowserAutofillSetting) {
return false;
}
if (!(await this.privacyPermissionGranted())) {
return false;
}
return await BrowserApi.browserAutofillSettingsOverridden();
}
async privacyPermissionGranted(): Promise<boolean> {
return await BrowserApi.permissionsGranted(["privacy"]);
}
async updateContextMenuItem() {
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
this.messagingService.send("bgUpdateContextMenu");
}
async updateAutoTotpCopy() {
await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy);
}
async saveClearClipboard() {
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
}
async updateShowCardsCurrentTab() {
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
}
async updateShowIdentitiesCurrentTab() {
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
}
async updateShowInlineMenuCards() {
await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards);
}
async updateShowInlineMenuIdentities() {
await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities);
}
}

View File

@ -1,91 +0,0 @@
<form #form (ngSubmit)="submit()">
<header>
<div class="left">
<button type="button" routerLink="/notifications">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "excludedDomains" | i18n }}</span>
</h1>
<div class="right">
<button type="submit">{{ "save" | i18n }}</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-footer" [ngStyle]="{ marginTop: '10px' }">
{{
accountSwitcherEnabled
? ("excludedDomainsDescAlt" | i18n)
: ("excludedDomainsDesc" | i18n)
}}
</div>
<ng-container *ngIf="excludedDomains">
<div
class="box-content-row box-content-row-multi"
appBoxRow
*ngFor="let domain of excludedDomains; let i = index; trackBy: trackByFunction"
>
<button
type="button"
appStopClick
(click)="removeUri(i)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<label for="excludedDomain{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
<input
id="excludedDomain{{ i }}"
name="excludedDomain{{ i }}"
type="text"
[(ngModel)]="domain.uri"
placeholder="{{ 'ex' | i18n }} https://google.com"
inputmode="url"
appInputVerbatim
/>
<label for="currentUris{{ i }}" class="sr-only">
{{ "currentUri" | i18n }} {{ i + 1 }}
</label>
<select
*ngIf="currentUris && currentUris.length"
id="currentUris{{ i }}"
name="currentUris{{ i }}"
[(ngModel)]="domain.uri"
[hidden]="!domain.showCurrentUris"
>
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
</select>
</div>
<div class="action-buttons">
<button
type="button"
*ngIf="currentUris && currentUris.length"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
(click)="toggleUriInput(domain)"
[attr.aria-pressed]="domain.showCurrentUris === true"
>
<i aria-hidden="true" class="bwi bwi-lg bwi-list"></i>
</button>
</div>
</div>
</ng-container>
<button
type="button"
appStopClick
(click)="addUri()"
class="box-content-row box-content-row-newmulti single-line"
>
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i> {{ "newUri" | i18n }}
</button>
</div>
</div>
</main>
</form>

View File

@ -1,143 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
interface ExcludedDomain {
uri: string;
showCurrentUris: boolean;
}
const BroadcasterSubscriptionId = "excludedDomains";
@Component({
selector: "app-excluded-domains-v1",
templateUrl: "excluded-domains-v1.component.html",
})
export class ExcludedDomainsV1Component implements OnInit, OnDestroy {
excludedDomains: ExcludedDomain[] = [];
existingExcludedDomains: ExcludedDomain[] = [];
currentUris: string[];
loadCurrentUrisTimeout: number;
accountSwitcherEnabled = false;
constructor(
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService,
private router: Router,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (savedDomains) {
for (const uri of Object.keys(savedDomains)) {
this.excludedDomains.push({ uri: uri, showCurrentUris: false });
this.existingExcludedDomains.push({ uri: uri, showCurrentUris: false });
}
}
await this.loadCurrentUris();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "tabChanged":
case "windowChanged":
if (this.loadCurrentUrisTimeout != null) {
window.clearTimeout(this.loadCurrentUrisTimeout);
}
this.loadCurrentUrisTimeout = window.setTimeout(
async () => await this.loadCurrentUris(),
500,
);
break;
default:
break;
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async addUri() {
this.excludedDomains.push({ uri: "", showCurrentUris: false });
}
async removeUri(i: number) {
this.excludedDomains.splice(i, 1);
}
async submit() {
const savedDomains: { [name: string]: null } = {};
const newExcludedDomains = this.getNewlyAddedDomains(this.excludedDomains);
for (const domain of this.excludedDomains) {
const resp = newExcludedDomains.filter((e) => e.uri === domain.uri);
if (resp.length === 0) {
savedDomains[domain.uri] = null;
} else {
if (domain.uri && domain.uri !== "") {
const validDomain = Utils.getHostname(domain.uri);
if (!validDomain) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("excludedDomainsInvalidDomain", domain.uri),
);
return;
}
savedDomains[validDomain] = null;
}
}
}
await this.domainSettingsService.setNeverDomains(savedDomains);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/settings"]);
}
trackByFunction(index: number, item: any) {
return index;
}
getNewlyAddedDomains(domain: ExcludedDomain[]): ExcludedDomain[] {
const result = this.excludedDomains.filter(
(newDomain) =>
!this.existingExcludedDomains.some((oldDomain) => newDomain.uri === oldDomain.uri),
);
return result;
}
toggleUriInput(domain: ExcludedDomain) {
domain.showCurrentUris = !domain.showCurrentUris;
}
async loadCurrentUris() {
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
if (tabs) {
const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url)));
uriSet.delete(null);
this.currentUris = Array.from(uriSet);
}
}
}

View File

@ -1,89 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "notifications" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
<input
id="use-passkeys"
type="checkbox"
aria-describedby="use-passkeysHelp"
(change)="updateEnablePasskeys()"
[(ngModel)]="enablePasskeys"
/>
</div>
</div>
<div id="use-passkeysHelp" class="box-footer">
{{ "usePasskeysDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
<input
id="addlogin-notification-bar"
type="checkbox"
aria-describedby="addlogin-notification-barHelp"
(change)="updateAddLoginNotification()"
[(ngModel)]="enableAddLoginNotification"
/>
</div>
</div>
<div id="addlogin-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("addLoginNotificationDescAlt" | i18n)
: ("addLoginNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="changedpass-notification-bar">{{
"enableChangedPasswordNotification" | i18n
}}</label>
<input
id="changedpass-notification-bar"
type="checkbox"
aria-describedby="changedpass-notification-barHelp"
(change)="updateChangedPasswordNotification()"
[(ngModel)]="enableChangedPasswordNotification"
/>
</div>
</div>
<div id="changedpass-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("changedPasswordNotificationDescAlt" | i18n)
: ("changedPasswordNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/excluded-domains"
>
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@ -1,53 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "autofill-notification-v1-settings",
templateUrl: "notifications-v1.component.html",
})
export class NotificationsSettingsV1Component implements OnInit {
enableAddLoginNotification = false;
enableChangedPasswordNotification = false;
enablePasskeys = true;
accountSwitcherEnabled = false;
constructor(
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private vaultSettingsService: VaultSettingsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
this.enableAddLoginNotification = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
this.enableChangedPasswordNotification = await firstValueFrom(
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
);
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
}
async updateAddLoginNotification() {
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
this.enableAddLoginNotification,
);
}
async updateChangedPasswordNotification() {
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
this.enableChangedPasswordNotification,
);
}
async updateEnablePasskeys() {
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
}
}

View File

@ -259,9 +259,7 @@ export default class RuntimeBackground {
await this.main.refreshBadge();
await this.main.refreshMenu(false);
if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) {
await this.autofillService.setAutoFillOnPageLoadOrgPolicy();
}
await this.autofillService.setAutoFillOnPageLoadOrgPolicy();
break;
}
case "addToLockedVaultPendingNotifications":
@ -288,9 +286,7 @@ export default class RuntimeBackground {
await this.configService.ensureConfigFetched();
await this.main.updateOverlayCiphers();
if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) {
await this.autofillService.setAutoFillOnPageLoadOrgPolicy();
}
await this.autofillService.setAutoFillOnPageLoadOrgPolicy();
}
break;
case "openPopup":

View File

@ -17,7 +17,6 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
@ -71,14 +70,10 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component";
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
@ -148,11 +143,12 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(Fido2V1Component, Fido2Component, {
{
path: "fido2",
component: Fido2Component,
canActivate: [fido2AuthGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
...twofactorRefactorSwap(
TwoFactorComponent,
AnonLayoutWrapperComponent,
@ -321,22 +317,24 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(AutofillV1Component, AutofillComponent, {
{
path: "autofill",
component: AutofillComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "account-security",
component: AccountSecurityComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
{
path: "notifications",
component: NotificationsSettingsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "vault-settings",
component: VaultSettingsV2Component,
@ -355,11 +353,12 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
{
path: "excluded-domains",
component: ExcludedDomainsComponent,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
}),
},
{
path: "premium",
component: PremiumV2Component,

View File

@ -35,17 +35,7 @@ import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component";
import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component";
import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component";
import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component";
import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component";
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
import { HeaderComponent } from "../platform/popup/header.component";
@ -87,10 +77,6 @@ import "../platform/popup/locales";
ScrollingModule,
ServicesModule,
DialogModule,
ExcludedDomainsComponent,
Fido2CipherRowComponent,
Fido2Component,
Fido2UseBrowserLinkComponent,
FilePopoutCalloutComponent,
AvatarModule,
AccountComponent,
@ -112,15 +98,11 @@ import "../platform/popup/locales";
ColorPasswordPipe,
ColorPasswordCountPipe,
EnvironmentComponent,
ExcludedDomainsV1Component,
Fido2CipherRowV1Component,
Fido2UseBrowserLinkV1Component,
HintComponent,
HomeComponent,
LoginViaAuthRequestComponentV1,
LoginComponentV1,
LoginDecryptionOptionsComponentV1,
NotificationsSettingsV1Component,
RegisterComponent,
SetPasswordComponent,
SsoComponentV1,
@ -131,8 +113,6 @@ import "../platform/popup/locales";
UserVerificationComponent,
VaultTimeoutInputComponent,
RemovePasswordComponent,
Fido2V1Component,
AutofillV1Component,
EnvironmentSelectorComponent,
],
exports: [],