mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
accountSwitcherEnabled ? ("excludedDomainsDescAlt" | i18n) : ("excludedDomainsDesc" | i18n)
|
||||
}}
|
||||
</p>
|
||||
<bit-section>
|
||||
<bit-section *ngIf="!isLoading">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "domainsTitle" | i18n }}</h2>
|
||||
<span bitTypography="body2" slot="end">{{ excludedDomainsState?.length || 0 }}</span>
|
||||
@@ -57,7 +57,13 @@
|
||||
</bit-section>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton buttonType="primary" type="submit" (click)="saveChanges()">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
[disabled]="dataIsPristine"
|
||||
(click)="saveChanges()"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { QueryList, Component, ElementRef, OnDestroy, OnInit, ViewChildren } from "@angular/core";
|
||||
import {
|
||||
QueryList,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
AfterViewInit,
|
||||
ViewChildren,
|
||||
} from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
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 { 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";
|
||||
@@ -29,8 +35,6 @@ import { PopupFooterComponent } from "../../../platform/popup/layout/popup-foote
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "excludedDomainsState";
|
||||
|
||||
@Component({
|
||||
selector: "app-excluded-domains",
|
||||
templateUrl: "excluded-domains.component.html",
|
||||
@@ -55,11 +59,12 @@ const BroadcasterSubscriptionId = "excludedDomainsState";
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChildren("uriInput") uriInputElements: QueryList<ElementRef<HTMLInputElement>>;
|
||||
|
||||
accountSwitcherEnabled = false;
|
||||
dataIsPristine = true;
|
||||
isLoading = false;
|
||||
excludedDomainsState: string[] = [];
|
||||
storedExcludedDomains: string[] = [];
|
||||
// How many fields should be non-editable before editable fields
|
||||
@@ -70,16 +75,27 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||
async ngAfterViewInit() {
|
||||
this.domainSettingsService.neverDomains$
|
||||
.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.storedExcludedDomains = Object.keys(neverDomains);
|
||||
}
|
||||
@@ -89,15 +105,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
// Do not allow the first x (pre-existing) fields to be edited
|
||||
this.fieldsEditThreshold = this.storedExcludedDomains.length;
|
||||
|
||||
this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
|
||||
this.focusNewUriInput(last);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.dataIsPristine = true;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
focusNewUriInput(elementRef: ElementRef) {
|
||||
@@ -116,7 +125,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
async removeDomain(i: number) {
|
||||
this.excludedDomainsState.splice(i, 1);
|
||||
|
||||
// if a pre-existing field was dropped, lower the edit threshold
|
||||
// If a pre-existing field was dropped, lower the edit threshold
|
||||
if (i < this.fieldsEditThreshold) {
|
||||
this.fieldsEditThreshold--;
|
||||
}
|
||||
@@ -132,11 +141,11 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
|
||||
async saveChanges() {
|
||||
if (this.dataIsPristine) {
|
||||
await this.router.navigate(["/notifications"]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
const newExcludedDomainsSaveState: NeverDomains = {};
|
||||
const uniqueExcludedDomains = new Set(this.excludedDomainsState);
|
||||
|
||||
@@ -151,6 +160,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
this.i18nService.t("excludedDomainsInvalidDomain", uri),
|
||||
);
|
||||
|
||||
// Don't reset via `handleStateUpdate` to allow existing input value correction
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +170,23 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
|
||||
const existingState = new Set(this.storedExcludedDomains);
|
||||
const newState = new Set(Object.keys(newExcludedDomainsSaveState));
|
||||
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.storedExcludedDomains.reduce(
|
||||
(neverDomains, uri) => ({ ...neverDomains, [uri]: null }),
|
||||
{},
|
||||
);
|
||||
this.handleStateUpdate(constructedNeverDomainsState);
|
||||
} else {
|
||||
await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
@@ -169,11 +196,9 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
||||
} catch {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||
|
||||
// Do not navigate on error
|
||||
return;
|
||||
// Don't reset via `handleStateUpdate` to preserve input values
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/notifications"]);
|
||||
}
|
||||
|
||||
trackByFunction(index: number, _: string) {
|
||||
|
||||
@@ -472,7 +472,7 @@
|
||||
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u)"
|
||||
(click)="launch(u, cipher.id)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@@ -348,15 +348,13 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
launch(uri: Launchable, cipherId?: string) {
|
||||
async launch(uri: Launchable, cipherId?: string) {
|
||||
if (!uri.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cipherId) {
|
||||
// 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.cipherService.updateLastLaunchedDate(cipherId);
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId);
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
|
||||
@@ -8,6 +8,14 @@ export enum EncryptionType {
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
|
||||
}
|
||||
|
||||
export function encryptionTypeToString(encryptionType: EncryptionType): string {
|
||||
if (encryptionType in EncryptionType) {
|
||||
return EncryptionType[encryptionType];
|
||||
} else {
|
||||
return "Unknown encryption type " + encryptionType;
|
||||
}
|
||||
}
|
||||
|
||||
/** The expected number of parts to a serialized EncString of the given encryption type.
|
||||
* For example, an EncString of type AesCbc256_B64 will have 2 parts, and an EncString of type
|
||||
* AesCbc128_HmacSha256_B64 will have 3 parts.
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Utils } from "../../../platform/misc/utils";
|
||||
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../abstractions/encrypt.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { EncryptionType, encryptionTypeToString as encryptionTypeName } from "../../enums";
|
||||
import { Decryptable } from "../../interfaces/decryptable.interface";
|
||||
import { Encrypted } from "../../interfaces/encrypted";
|
||||
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
|
||||
@@ -70,13 +70,24 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
|
||||
key = this.resolveLegacyKey(key, encString);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encString?.mac == null) {
|
||||
this.logService.error("MAC required but not provided.");
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encString.encryptionType) {
|
||||
this.logService.error("Key encryption type does not match payload encryption type.");
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -94,7 +105,12 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
);
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logMacFailed("MAC comparison failed. Key or payload has changed.");
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -113,13 +129,24 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
|
||||
key = this.resolveLegacyKey(key, encThing);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encThing.macBytes == null) {
|
||||
this.logService.error("MAC required but not provided.");
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encThing.encryptionType) {
|
||||
this.logService.error("Key encryption type does not match payload encryption type.");
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,13 +156,25 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
|
||||
if (computedMac === null) {
|
||||
this.logMacFailed("Failed to compute MAC.");
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] Failed to compute MAC." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logMacFailed("MAC comparison failed. Key or payload has changed.");
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] MAC comparison failed. Key or payload has changed." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +203,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
|
||||
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (data == null) {
|
||||
throw new Error("No data provided for decryption.");
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
|
||||
}
|
||||
|
||||
let algorithm: "sha1" | "sha256";
|
||||
@@ -182,7 +221,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key provided for decryption.");
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption.");
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
|
||||
|
||||
@@ -650,14 +650,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
ciphersLocalData = {};
|
||||
}
|
||||
|
||||
const cipherId = id as CipherId;
|
||||
if (ciphersLocalData[cipherId]) {
|
||||
ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
|
||||
} else {
|
||||
ciphersLocalData[cipherId] = {
|
||||
lastUsedDate: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
const currentTime = new Date().getTime();
|
||||
ciphersLocalData[id as CipherId] = {
|
||||
lastLaunched: currentTime,
|
||||
lastUsedDate: currentTime,
|
||||
};
|
||||
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
|
||||
|
||||
@@ -56,7 +56,12 @@
|
||||
<form [formGroup]="username" class="box tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||
<bit-select [items]="usernameOptions$ | async" formControlName="nav"> </bit-select>
|
||||
<bit-select
|
||||
[items]="usernameOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="username-type"
|
||||
>
|
||||
</bit-select>
|
||||
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
|
||||
credentialTypeHint$ | async
|
||||
}}</bit-hint>
|
||||
@@ -65,7 +70,12 @@
|
||||
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="box tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select [items]="forwarderOptions$ | async" formControlName="nav"> </bit-select>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
isUsernameAlgorithm,
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
// constants used to identify navigation selections that are not
|
||||
// generator algorithms
|
||||
@@ -51,6 +52,7 @@ const NONE_SELECTED = "none";
|
||||
export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private generatorHistoryService: GeneratorHistoryService,
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
@@ -182,9 +184,16 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((generated) => {
|
||||
.subscribe(([generated, userId]) => {
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
@@ -14,7 +15,9 @@ import {
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { Option } from "@bitwarden/components/src/select/option";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
isPasswordAlgorithm,
|
||||
AlgorithmInfo,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
/** Options group for passwords */
|
||||
@Component({
|
||||
@@ -34,6 +38,9 @@ import {
|
||||
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private generatorHistoryService: GeneratorHistoryService,
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
private zone: NgZone,
|
||||
@@ -109,10 +116,32 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
// wire up the generator
|
||||
this.algorithm$
|
||||
.pipe(
|
||||
filter((algorithm) => !!algorithm),
|
||||
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
||||
catchError((error: unknown, generator) => {
|
||||
if (typeof error === "string") {
|
||||
this.toastService.showToast({
|
||||
message: error,
|
||||
variant: "error",
|
||||
title: "",
|
||||
});
|
||||
} else {
|
||||
this.logService.error(error);
|
||||
}
|
||||
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((generated) => {
|
||||
.subscribe(([generated, userId]) => {
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -33,7 +33,12 @@
|
||||
<form class="box" [formGroup]="username" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||
<bit-select [items]="typeOptions$ | async" formControlName="nav"> </bit-select>
|
||||
<bit-select
|
||||
[items]="typeOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="username-type"
|
||||
>
|
||||
</bit-select>
|
||||
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
|
||||
credentialTypeHint$ | async
|
||||
}}</bit-hint>
|
||||
@@ -42,7 +47,12 @@
|
||||
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="box tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "service" | i18n }}</bit-label>
|
||||
<bit-select [items]="forwarderOptions$ | async" formControlName="nav"> </bit-select>
|
||||
<bit-select
|
||||
[items]="forwarderOptions$ | async"
|
||||
formControlName="nav"
|
||||
data-testid="email-forwarding-service"
|
||||
>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
isUsernameAlgorithm,
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
// constants used to identify navigation selections that are not
|
||||
// generator algorithms
|
||||
@@ -57,6 +58,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
constructor(
|
||||
private generatorService: CredentialGeneratorService,
|
||||
private generatorHistoryService: GeneratorHistoryService,
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
@@ -153,9 +155,16 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
// continue with origin stream
|
||||
return generator;
|
||||
}),
|
||||
withLatestFrom(this.userId$),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe((generated) => {
|
||||
.subscribe(([generated, userId]) => {
|
||||
this.generatorHistoryService
|
||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||
.catch((e: unknown) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { GeneratorCategory } from "./options";
|
||||
import { CredentialAlgorithm } from "@bitwarden/generator-core";
|
||||
|
||||
/** A credential generation result */
|
||||
export class GeneratedCredential {
|
||||
@@ -14,7 +14,7 @@ export class GeneratedCredential {
|
||||
*/
|
||||
constructor(
|
||||
readonly credential: string,
|
||||
readonly category: GeneratorCategory,
|
||||
readonly category: CredentialAlgorithm,
|
||||
generationDate: Date | number,
|
||||
) {
|
||||
if (typeof generationDate === "number") {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CredentialAlgorithm } from "@bitwarden/generator-core";
|
||||
|
||||
import { GeneratedCredential } from "./generated-credential";
|
||||
import { GeneratorCategory } from "./options";
|
||||
|
||||
/** Tracks the history of password generations.
|
||||
* Each user gets their own store.
|
||||
@@ -27,7 +27,7 @@ export abstract class GeneratorHistoryService {
|
||||
track: (
|
||||
userId: UserId,
|
||||
credential: string,
|
||||
category: GeneratorCategory,
|
||||
category: CredentialAlgorithm,
|
||||
date?: Date,
|
||||
) => Promise<GeneratedCredential | null>;
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-pack
|
||||
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
|
||||
import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CredentialAlgorithm } from "@bitwarden/generator-core";
|
||||
|
||||
import { GeneratedCredential } from "./generated-credential";
|
||||
import { GeneratorHistoryService } from "./generator-history.abstraction";
|
||||
import { GENERATOR_HISTORY, GENERATOR_HISTORY_BUFFER } from "./key-definitions";
|
||||
import { LegacyPasswordHistoryDecryptor } from "./legacy-password-history-decryptor";
|
||||
import { GeneratorCategory, HistoryServiceOptions } from "./options";
|
||||
import { HistoryServiceOptions } from "./options";
|
||||
|
||||
const OPTIONS_FRAME_SIZE = 2048;
|
||||
|
||||
@@ -25,7 +26,7 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly keyService: CryptoService,
|
||||
private readonly stateProvider: StateProvider,
|
||||
private readonly options: HistoryServiceOptions = { maxTotal: 100 },
|
||||
private readonly options: HistoryServiceOptions = { maxTotal: 200 },
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -33,7 +34,12 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
|
||||
private _credentialStates = new Map<UserId, SingleUserState<GeneratedCredential[]>>();
|
||||
|
||||
/** {@link GeneratorHistoryService.track} */
|
||||
track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => {
|
||||
track = async (
|
||||
userId: UserId,
|
||||
credential: string,
|
||||
category: CredentialAlgorithm,
|
||||
date?: Date,
|
||||
) => {
|
||||
const state = this.getCredentialState(userId);
|
||||
let result: GeneratedCredential = null;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, Input } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import {
|
||||
CardComponent,
|
||||
@@ -30,10 +31,15 @@ import {
|
||||
})
|
||||
export class AutofillOptionsViewComponent {
|
||||
@Input() loginUris: LoginUriView[];
|
||||
@Input() cipherId: string;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
openWebsite(selectedUri: string) {
|
||||
async openWebsite(selectedUri: string) {
|
||||
await this.cipherService.updateLastLaunchedDate(this.cipherId);
|
||||
this.platformUtilsService.launchUri(selectedUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
<app-login-credentials-view *ngIf="hasLogin" [cipher]="cipher"></app-login-credentials-view>
|
||||
|
||||
<!-- AUTOFILL OPTIONS -->
|
||||
<app-autofill-options-view *ngIf="hasAutofill" [loginUris]="cipher.login.uris">
|
||||
<app-autofill-options-view
|
||||
*ngIf="hasAutofill"
|
||||
[loginUris]="cipher.login.uris"
|
||||
[cipherId]="cipher.id"
|
||||
>
|
||||
</app-autofill-options-view>
|
||||
|
||||
<!-- CARD DETAILS -->
|
||||
|
||||
Reference in New Issue
Block a user