mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 02:23:25 +00:00
merge in main
This commit is contained in:
@@ -116,7 +116,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
const promises = collections.map(async (c) => {
|
||||
const view = new CollectionAdminView();
|
||||
view.id = c.id;
|
||||
view.name = await this.encryptService.decryptToUtf8(new EncString(c.name), orgKey);
|
||||
view.name = await this.encryptService.decryptString(new EncString(c.name), orgKey);
|
||||
view.externalId = c.externalId;
|
||||
view.organizationId = c.organizationId;
|
||||
|
||||
@@ -146,7 +146,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
}
|
||||
const collection = new CollectionRequest();
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = (await this.encryptService.encrypt(model.name, key)).encryptedString;
|
||||
collection.name = (await this.encryptService.encryptString(model.name, key)).encryptedString;
|
||||
collection.groups = model.groups.map(
|
||||
(group) =>
|
||||
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage),
|
||||
|
||||
@@ -120,7 +120,7 @@ const mockStateProvider = () => {
|
||||
const mockCryptoService = () => {
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
encryptService.decryptToUtf8
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.anything())
|
||||
.mockResolvedValue("DECRYPTED_STRING");
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export class DefaultCollectionService implements CollectionService {
|
||||
collection.organizationId = model.organizationId;
|
||||
collection.readOnly = model.readOnly;
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = await this.encryptService.encrypt(model.name, key);
|
||||
collection.name = await this.encryptService.encryptString(model.name, key);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ describe("DefaultvNextCollectionService", () => {
|
||||
keyService.orgKeys$.mockReturnValue(cryptoKeys);
|
||||
|
||||
// Set up mock decryption
|
||||
encryptService.decryptToUtf8
|
||||
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey), expect.any(String))
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey))
|
||||
.mockImplementation((encString, key) =>
|
||||
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
|
||||
);
|
||||
@@ -103,15 +103,14 @@ describe("DefaultvNextCollectionService", () => {
|
||||
]);
|
||||
|
||||
// Assert that the correct org keys were used for each encrypted string
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
// This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection1.name)),
|
||||
orgKey1,
|
||||
expect.any(String),
|
||||
);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection2.name)),
|
||||
orgKey2,
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export class DefaultvNextCollectionService implements vNextCollectionService {
|
||||
collection.organizationId = model.organizationId;
|
||||
collection.readOnly = model.readOnly;
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = await this.encryptService.encrypt(model.name, key);
|
||||
collection.name = await this.encryptService.encryptString(model.name, key);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -50,9 +52,7 @@ export class CollectionsComponent implements OnInit {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
this.collections = await this.loadCollections();
|
||||
|
||||
this.collections.forEach((c) => ((c as any).checked = false));
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angu
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
EnvironmentService,
|
||||
@@ -57,6 +59,7 @@ export interface EnvironmentSelectorRouteData {
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter<void>();
|
||||
@@ -110,16 +113,16 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (
|
||||
option === Region.SelfHosted &&
|
||||
(await SelfHostedEnvConfigDialogComponent.open(this.dialogService))
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
|
||||
if (option === Region.SelfHosted) {
|
||||
const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
|
||||
if (dialogResult) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
// Don't proceed to setEnvironment when the self-hosted dialog is cancelled
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@ import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { filter, first, switchMap, tap } from "rxjs/operators";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
||||
@@ -10,6 +10,7 @@ import { WebAuthnIcon } from "../icons/webauthn.icon";
|
||||
@Component({
|
||||
selector: "auth-two-factor-icon",
|
||||
templateUrl: "./two-factor-icon.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class TwoFactorIconComponent {
|
||||
@Input() provider: any;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
*/
|
||||
@Directive({
|
||||
selector: "app-user-verification",
|
||||
standalone: false,
|
||||
})
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -2,6 +2,8 @@ import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import {
|
||||
@@ -30,9 +30,7 @@ describe("AuthGuard", () => {
|
||||
authService.getAuthStatus.mockResolvedValue(authStatus);
|
||||
const messagingService: MockProxy<MessagingService> = mock<MessagingService>();
|
||||
const keyConnectorService: MockProxy<KeyConnectorService> = mock<KeyConnectorService>();
|
||||
keyConnectorService.getConvertAccountRequired.mockResolvedValue(
|
||||
keyConnectorServiceRequiresAccountConversion,
|
||||
);
|
||||
keyConnectorService.convertAccountRequired$ = of(keyConnectorServiceRequiresAccountConversion);
|
||||
const accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const authGuard: CanActivateFn = async (
|
||||
|
||||
if (
|
||||
!routerState.url.includes("remove-password") &&
|
||||
(await keyConnectorService.getConvertAccountRequired())
|
||||
(await firstValueFrom(keyConnectorService.convertAccountRequired$))
|
||||
) {
|
||||
return router.createUrlTree(["/remove-password"]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { merge, Observable, tap } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EMPTY, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -22,6 +22,8 @@ export type AddAccountCreditDialogParams = {
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum AddAccountCreditDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
@@ -46,6 +48,7 @@ type PayPalConfig = {
|
||||
|
||||
@Component({
|
||||
templateUrl: "./add-account-credit-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddAccountCreditDialogComponent implements OnInit {
|
||||
@ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
|
||||
@Component({
|
||||
selector: "app-invoices",
|
||||
templateUrl: "./invoices.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class InvoicesComponent implements OnInit {
|
||||
@Input() startWith?: InvoicesResponse;
|
||||
|
||||
@@ -30,6 +30,7 @@ const partnerTrustIcon = svgIcon`
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
<p class="tw-mt-4">{{ "noInvoicesToList" | i18n }}</p>
|
||||
</div>`,
|
||||
standalone: false,
|
||||
})
|
||||
export class NoInvoicesComponent {
|
||||
icon = partnerTrustIcon;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/model
|
||||
@Component({
|
||||
selector: "app-manage-tax-information",
|
||||
templateUrl: "./manage-tax-information.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
@Input() startWith: TaxInformation;
|
||||
@@ -64,12 +65,8 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
validate(): boolean {
|
||||
if (this.formGroup.dirty) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
} else {
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
this.formGroup.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
|
||||
markAllAsTouched() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appNotPremium]",
|
||||
standalone: false,
|
||||
})
|
||||
export class NotPremiumDirective implements OnInit {
|
||||
constructor(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CalloutTypes } from "@bitwarden/components";
|
||||
@Component({
|
||||
selector: "app-callout",
|
||||
templateUrl: "callout.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class DeprecatedCalloutComponent implements OnInit {
|
||||
@Input() type: CalloutTypes = "info";
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ModalRef } from "./modal.ref";
|
||||
@Component({
|
||||
selector: "app-modal",
|
||||
template: "<ng-template #modalContent></ng-template>",
|
||||
standalone: false,
|
||||
})
|
||||
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
|
||||
componentRef: ComponentRef<any>;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
@@ -76,9 +78,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
this.cipher = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||
}
|
||||
|
||||
filterCollections() {
|
||||
@@ -105,9 +105,7 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const cipherView = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId);
|
||||
const orgs = await firstValueFrom(this.organizations$);
|
||||
const orgName =
|
||||
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Subscription } from "rxjs";
|
||||
|
||||
@Directive({
|
||||
selector: "[appA11yInvalid]",
|
||||
standalone: false,
|
||||
})
|
||||
export class A11yInvalidDirective implements OnDestroy, OnInit {
|
||||
private sub: Subscription;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appApiAction]",
|
||||
standalone: false,
|
||||
})
|
||||
export class ApiActionDirective implements OnChanges {
|
||||
@Input() appApiAction: Promise<any>;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appBoxRow]",
|
||||
standalone: false,
|
||||
})
|
||||
export class BoxRowDirective implements OnInit {
|
||||
el: HTMLElement = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyText]",
|
||||
standalone: false,
|
||||
})
|
||||
export class CopyTextDirective {
|
||||
constructor(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Directive, ElementRef, HostListener, Input } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appFallbackSrc]",
|
||||
standalone: false,
|
||||
})
|
||||
export class FallbackSrcDirective {
|
||||
@Input("appFallbackSrc") appFallbackSrc: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appIfFeature]",
|
||||
standalone: false,
|
||||
})
|
||||
export class IfFeatureDirective implements OnInit {
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NgControl } from "@angular/forms";
|
||||
|
||||
@Directive({
|
||||
selector: "input[appInputStripSpaces]",
|
||||
standalone: false,
|
||||
})
|
||||
export class InputStripSpacesDirective {
|
||||
constructor(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appInputVerbatim]",
|
||||
standalone: false,
|
||||
})
|
||||
export class InputVerbatimDirective implements OnInit {
|
||||
@Input() set appInputVerbatim(condition: boolean | string) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@Directive({
|
||||
selector: "[appLaunchClick]",
|
||||
standalone: false,
|
||||
})
|
||||
export class LaunchClickDirective {
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Directive, HostListener } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appStopClick]",
|
||||
standalone: false,
|
||||
})
|
||||
export class StopClickDirective {
|
||||
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Directive, HostListener } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appStopProp]",
|
||||
standalone: false,
|
||||
})
|
||||
export class StopPropDirective {
|
||||
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: false,
|
||||
})
|
||||
export class TrueFalseValueDirective implements ControlValueAccessor {
|
||||
@Input() trueValue: boolean | string = true;
|
||||
|
||||
@@ -7,7 +7,10 @@ import { ColorPasswordPipe } from "./color-password.pipe";
|
||||
/*
|
||||
An updated pipe that extends ColourPasswordPipe to include a character count
|
||||
*/
|
||||
@Pipe({ name: "colorPasswordCount" })
|
||||
@Pipe({
|
||||
name: "colorPasswordCount",
|
||||
standalone: false,
|
||||
})
|
||||
export class ColorPasswordCountPipe extends ColorPasswordPipe {
|
||||
transform(password: string) {
|
||||
const template = (character: string, type: string, index: number) =>
|
||||
|
||||
@@ -6,7 +6,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
An updated pipe that sanitizes HTML, highlights numbers and special characters (in different colors each)
|
||||
and handles Unicode / Emoji characters correctly.
|
||||
*/
|
||||
@Pipe({ name: "colorPassword" })
|
||||
@Pipe({
|
||||
name: "colorPassword",
|
||||
standalone: false,
|
||||
})
|
||||
export class ColorPasswordPipe implements PipeTransform {
|
||||
transform(password: string) {
|
||||
const template = (character: string, type: string) =>
|
||||
|
||||
@@ -28,7 +28,10 @@ const numberFormats: Record<string, CardRuleEntry[]> = {
|
||||
Other: [{ cardLength: 16, blocks: [4, 4, 4, 4] }],
|
||||
};
|
||||
|
||||
@Pipe({ name: "creditCardNumber" })
|
||||
@Pipe({
|
||||
name: "creditCardNumber",
|
||||
standalone: false,
|
||||
})
|
||||
export class CreditCardNumberPipe implements PipeTransform {
|
||||
transform(creditCardNumber: string, brand: string): string {
|
||||
let rules = numberFormats[brand];
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Pipe({
|
||||
name: "searchCiphers",
|
||||
standalone: false,
|
||||
})
|
||||
export class SearchCiphersPipe implements PipeTransform {
|
||||
transform(ciphers: CipherView[], searchText: string, deleted = false): CipherView[] {
|
||||
|
||||
@@ -6,6 +6,7 @@ type PropertyValueFunction<T> = (item: T) => { toString: () => string };
|
||||
|
||||
@Pipe({
|
||||
name: "search",
|
||||
standalone: false,
|
||||
})
|
||||
export class SearchPipe implements PipeTransform {
|
||||
transform<T>(
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface User {
|
||||
|
||||
@Pipe({
|
||||
name: "userName",
|
||||
standalone: false,
|
||||
})
|
||||
export class UserNamePipe implements PipeTransform {
|
||||
transform(user?: User): string {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
|
||||
@Pipe({
|
||||
name: "userType",
|
||||
standalone: false,
|
||||
})
|
||||
export class UserTypePipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
@Pipe({
|
||||
name: "ellipsis",
|
||||
standalone: false,
|
||||
})
|
||||
/**
|
||||
* @deprecated Use the tailwind class 'tw-truncate' instead
|
||||
|
||||
@@ -5,6 +5,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Pipe({
|
||||
name: "fingerprint",
|
||||
standalone: false,
|
||||
})
|
||||
export class FingerprintPipe {
|
||||
constructor(private keyService: KeyService) {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
*/
|
||||
@Pipe({
|
||||
name: "i18n",
|
||||
standalone: false,
|
||||
})
|
||||
export class I18nPipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
130
libs/angular/src/platform/view-cache/README.md
Normal file
130
libs/angular/src/platform/view-cache/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Extension Persistence
|
||||
|
||||
By default, when the browser extension popup closes, the user's current view and any data entered
|
||||
without saving is lost. This introduces friction in several workflows within our client, such as:
|
||||
|
||||
- Performing actions that require email OTP entry, since the user must navigate from the popup to
|
||||
get to their email inbox
|
||||
- Entering information to create a new vault item from a browser tab
|
||||
- And many more
|
||||
|
||||
Previously, we have recommended that users "pop out" the extension into its own window to persist
|
||||
the extension context, but this introduces additional user actions and may leave the extension open
|
||||
(and unlocked) for longer than a user intends.
|
||||
|
||||
In order to provide a better user experience, we have introduced two levels of persistence to the
|
||||
Bitwarden extension client:
|
||||
|
||||
- We persist the route history, allowing us to re-open the last route when the popup re-opens, and
|
||||
- We offer a service for teams to use to persist component-specific form data or state to survive a
|
||||
popup close/re-open cycle
|
||||
|
||||
## Persistence lifetime
|
||||
|
||||
Since we are persisting data, it is important that the lifetime of that data be well-understood and
|
||||
well-constrained. The cache of route history and form data is cleared when any of the following
|
||||
events occur:
|
||||
|
||||
- The account is locked
|
||||
- The account is logged out
|
||||
- Account switching is used to switch the active account
|
||||
- The extension popup has been closed for 2 minutes
|
||||
|
||||
In addition, cached form data is cleared when a browser extension navigation event occurs (e.g.
|
||||
switching between tabs in the extension).
|
||||
|
||||
## Types of persistence
|
||||
|
||||
### Route history persistence
|
||||
|
||||
Route history is persisted on the extension automatically, with no specific implementation required
|
||||
on any component.
|
||||
|
||||
The persistence layer ensures that the popup will open at the same route as was active when it
|
||||
closed, provided that none of the lifetime expiration events have occurred.
|
||||
|
||||
:::tip Excluding a route
|
||||
|
||||
If a particular route should be excluded from the history and not persisted, add
|
||||
`doNotSaveUrl: true` to the `data` property on the route.
|
||||
|
||||
:::
|
||||
|
||||
### View data persistence
|
||||
|
||||
Route persistence ensures that the user will land back on the route that they were on when the popup
|
||||
closed, but it does not persist any state or form data that the user may have modified. In order to
|
||||
persist that data, the component is responsible for registering that data with the
|
||||
[`ViewCacheService`](./view-cache.service.ts).
|
||||
This is done prescriptively to ensure that only necessary data is cached and that it is done with
|
||||
intention by the component.
|
||||
|
||||
The `ViewCacheService` provides an interface for caching both individual state and `FormGroup`s.
|
||||
|
||||
#### Caching individual data elements
|
||||
|
||||
For individual pieces of state, use the `signal()` method on the `ViewCacheService` to create a
|
||||
writeable [signal](https://angular.dev/guide/signals) wrapper around the desired state.
|
||||
|
||||
```typescript
|
||||
const mySignal = this.viewCacheService.signal({
|
||||
key: "my-state-key"
|
||||
initialValue: null
|
||||
});
|
||||
```
|
||||
|
||||
If a cached value exists, the returned signal will contain the cached data.
|
||||
|
||||
Setting the value should be done through the signal's `set()` method:
|
||||
|
||||
```typescript
|
||||
const mySignal = this.viewCacheService.signal({
|
||||
key: "my-state-key"
|
||||
initialValue: null
|
||||
});
|
||||
mySignal.set("value")
|
||||
```
|
||||
|
||||
:::note Equality comparison
|
||||
|
||||
By default, signals use `Object.is` to determine equality, and `set()` will only trigger updates if
|
||||
the updated value is not equal to the current signal state. See documentation
|
||||
[here](https://angular.dev/guide/signals#signal-equality-functions).
|
||||
|
||||
:::
|
||||
|
||||
Putting this together, the most common implementation pattern would be:
|
||||
|
||||
1. **Register the signal** using `ViewCacheService.signal()` on initialization of the component or
|
||||
service responsible for the state being persisted.
|
||||
2. **Restore state from the signal:** If cached data exists, the signal will contain that data. The
|
||||
component or service should use this data to re-create the state from prior to the popup closing.
|
||||
3. **Set new state** in the cache when it changes. Ensure that any updates to the data are persisted
|
||||
to the cache with `set()`, so that the cache reflects the latest state.
|
||||
|
||||
#### Caching form data
|
||||
|
||||
For persisting form data, the `ViewCacheService` supplies a `formGroup()` method, which manages the
|
||||
persistence of any entered form data to the cache and the initialization of the form from the cached
|
||||
data. You can supply the `FormGroup` in the `control` parameter of the method, and the
|
||||
`ViewCacheService` will:
|
||||
|
||||
- Initialize the form the a cached value, if it exists
|
||||
- Save form value to cache when it changes
|
||||
- Mark the form dirty if the restored value is not `undefined`.
|
||||
|
||||
```typescript
|
||||
this.loginDetailsForm = this.viewCacheService.formGroup({
|
||||
key: "my-form",
|
||||
control: this.formBuilder.group({
|
||||
username: [""],
|
||||
email: [""],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## What about other clients?
|
||||
|
||||
The `ViewCacheService` is designed to be injected into shared, client-agnostic components. A
|
||||
`NoopViewCacheService` is provided and injected for non-extension clients, preserving a single
|
||||
interface for your components.
|
||||
1
libs/angular/src/platform/view-cache/index.ts
Normal file
1
libs/angular/src/platform/view-cache/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ViewCacheService, FormCacheOptions, SignalCacheOptions } from "./view-cache.service";
|
||||
1
libs/angular/src/platform/view-cache/internal.ts
Normal file
1
libs/angular/src/platform/view-cache/internal.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { NoopViewCacheService } from "./noop-view-cache.service";
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Injectable, signal, WritableSignal } from "@angular/core";
|
||||
import type { FormGroup } from "@angular/forms";
|
||||
|
||||
import {
|
||||
FormCacheOptions,
|
||||
SignalCacheOptions,
|
||||
ViewCacheService,
|
||||
} from "../abstractions/view-cache.service";
|
||||
import { FormCacheOptions, SignalCacheOptions, ViewCacheService } from "./view-cache.service";
|
||||
|
||||
/**
|
||||
* The functionality of the {@link ViewCacheService} is only needed in the browser extension popup,
|
||||
@@ -23,6 +23,12 @@ type BaseCacheOptions<T> = {
|
||||
* Optional flag to persist the cached value between navigation events.
|
||||
*/
|
||||
persistNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* When set, the cached value will be cleared when the user changes tabs.
|
||||
* @optional
|
||||
*/
|
||||
clearOnTabChange?: true;
|
||||
} & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
|
||||
|
||||
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {
|
||||
@@ -42,6 +48,8 @@ export type FormCacheOptions<TFormGroup extends FormGroup> = BaseCacheOptions<
|
||||
/**
|
||||
* Cache for temporary component state
|
||||
*
|
||||
* [Read more](./README.md)
|
||||
*
|
||||
* #### Implementations
|
||||
* - browser extension popup: used to persist UI between popup open and close
|
||||
* - all other clients: noop
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "DM Sans";
|
||||
font-family: Roboto;
|
||||
src:
|
||||
url("webfonts/dm-sans.woff2") format("woff2 supports variations"),
|
||||
url("webfonts/dm-sans.woff2") format("woff2-variations");
|
||||
url("webfonts/roboto.woff2") format("woff2 supports variations"),
|
||||
url("webfonts/roboto.woff2") format("woff2-variations");
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
libs/angular/src/scss/webfonts/roboto.woff2
Normal file
BIN
libs/angular/src/scss/webfonts/roboto.woff2
Normal file
Binary file not shown.
@@ -2,6 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { VaultTimeout } from "@bitwarden/common/key-management/vault-timeout";
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
CollectionService,
|
||||
DefaultCollectionService,
|
||||
DefaultOrganizationUserApiService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
@@ -27,7 +31,11 @@ import {
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthEmailComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
ChangePasswordService,
|
||||
DefaultChangePasswordService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
AuthRequestApiService,
|
||||
AuthRequestService,
|
||||
@@ -263,6 +271,7 @@ import {
|
||||
InternalSendService,
|
||||
SendService as SendServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
@@ -281,6 +290,7 @@ import {
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
@@ -312,6 +322,8 @@ import {
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
IndividualVaultExportService,
|
||||
@@ -325,13 +337,14 @@ import {
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
|
||||
import { NoopViewCacheService } from "../platform/services/noop-view-cache.service";
|
||||
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
|
||||
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
|
||||
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
|
||||
import { ViewCacheService } from "../platform/view-cache";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import { NoopViewCacheService } from "../platform/view-cache/internal";
|
||||
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
@@ -508,6 +521,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountServiceAbstraction,
|
||||
logService: LogService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
) =>
|
||||
new CipherService(
|
||||
keyService,
|
||||
@@ -524,6 +538,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateProvider,
|
||||
accountService,
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
),
|
||||
deps: [
|
||||
KeyService,
|
||||
@@ -540,6 +555,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
AccountServiceAbstraction,
|
||||
LogService,
|
||||
CipherEncryptionService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1078,7 +1094,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: OrganizationSponsorshipApiServiceAbstraction,
|
||||
useClass: OrganizationSponsorshipApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
deps: [ApiServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationBillingApiServiceAbstraction,
|
||||
@@ -1527,6 +1543,20 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: MasterPasswordApiService,
|
||||
deps: [ApiServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherEncryptionService,
|
||||
useClass: DefaultCipherEncryptionService,
|
||||
deps: [SdkService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangePasswordService,
|
||||
useClass: DefaultChangePasswordService,
|
||||
deps: [
|
||||
KeyService,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -3,6 +3,8 @@ import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { openPasswordHistoryDialog } from "@bitwarden/vault";
|
||||
|
||||
import { VaultViewPasswordHistoryService } from "./view-password-history.service";
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Injectable } from "@angular/core";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { openPasswordHistoryDialog } from "@bitwarden/vault";
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface PasswordColorText {
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() showText = false;
|
||||
|
||||
@@ -36,6 +36,8 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service.
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
// Value = hours
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
enum DatePreset {
|
||||
OneHour = 1,
|
||||
OneDay = 24,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DatePipe } from "@angular/common";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { concatMap, firstValueFrom, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -40,6 +42,8 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@Directive()
|
||||
@@ -269,9 +273,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
if (this.cipher == null) {
|
||||
if (this.editMode) {
|
||||
const cipher = await this.loadCipher(activeUserId);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
// Adjust Cipher Name if Cloning
|
||||
if (this.cloneMode) {
|
||||
|
||||
@@ -9,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -56,6 +56,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -88,9 +89,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||
this.cipherDomain = await this.formPromise;
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -130,9 +129,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
const updatedCipher = await this.deletePromises[attachment.id];
|
||||
|
||||
const cipher = new Cipher(updatedCipher);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -197,12 +194,14 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipherDomain.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
@@ -228,9 +227,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected async init() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||
@@ -276,15 +273,17 @@ export class AttachmentsComponent implements OnInit {
|
||||
|
||||
try {
|
||||
// 2. Resave
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipherDomain.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
attachment.fileName,
|
||||
@@ -292,9 +291,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
activeUserId,
|
||||
admin,
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
|
||||
|
||||
// 3. Delete old
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
||||
|
||||
@@ -19,6 +19,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
selector: "app-vault-icon",
|
||||
templateUrl: "icon.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class IconComponent {
|
||||
/**
|
||||
|
||||
@@ -42,9 +42,7 @@ export class PasswordHistoryComponent implements OnInit {
|
||||
protected async init() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
const decCipher = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
|
||||
<div>
|
||||
<h2 bitTypography="h4" class="tw-font-semibold !tw-mb-1">{{ title }}</h2>
|
||||
<p class="tw-text-main tw-mb-0" bitTypography="body2" [innerHTML]="subtitle"></p>
|
||||
<p
|
||||
*ngIf="subtitle"
|
||||
class="tw-text-main tw-mb-0"
|
||||
bitTypography="body2"
|
||||
[innerHTML]="subtitle"
|
||||
></p>
|
||||
<ng-content *ngIf="!subtitle"></ng-content>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -14,7 +14,7 @@ export class SpotlightComponent {
|
||||
// The title of the component
|
||||
@Input({ required: true }) title: string | null = null;
|
||||
// The subtitle of the component
|
||||
@Input({ required: true }) subtitle: string | null = null;
|
||||
@Input() subtitle?: string | null = null;
|
||||
// The text to display on the button
|
||||
@Input() buttonText?: string;
|
||||
// Wheter the component can be dismissed, if true, the component will not show a close button
|
||||
@@ -34,13 +34,13 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@@ -54,6 +54,8 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip
|
||||
import { TotpInfo } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "BaseViewComponent";
|
||||
@@ -95,7 +97,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
cipherType = CipherType;
|
||||
|
||||
private previousCipherId: string;
|
||||
private passwordReprompted = false;
|
||||
protected passwordReprompted = false;
|
||||
|
||||
/**
|
||||
* Represents TOTP information including display formatting and timing
|
||||
@@ -137,6 +139,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected toastService: ToastService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -458,19 +461,19 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
|
||||
this.cipher.id as CipherId,
|
||||
attachment,
|
||||
response,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.fileDownloadService.download({
|
||||
fileName: attachment.fileName,
|
||||
blobData: decBuf,
|
||||
});
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
|
||||
3
libs/angular/src/vault/index.ts
Normal file
3
libs/angular/src/vault/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Note: Nudge related code is exported from `libs/angular` because it is consumed by multiple
|
||||
// `libs/*` packages. Exporting from the `libs/vault` package creates circular dependencies.
|
||||
export { NudgesService, NudgeStatus, NudgeType } from "./services/nudges.service";
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, map } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
private pinService = inject(PinServiceAbstraction);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Failed to load profile date:");
|
||||
// Default to today to ensure the nudge is shown in case of an error
|
||||
return of(new Date());
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
from(this.pinService.isPinSet(userId)),
|
||||
from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || hideNudge,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -21,7 +21,7 @@ export class AutofillNudgeService extends DefaultSingleNudgeService {
|
||||
vaultProfileService = inject(VaultProfileService);
|
||||
logService = inject(LogService);
|
||||
|
||||
nudgeStatus$(_: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
nudgeStatus$(_: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Error getting profile creation date");
|
||||
@@ -32,7 +32,7 @@ export class AutofillNudgeService extends DefaultSingleNudgeService {
|
||||
|
||||
return combineLatest([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(VaultNudgeType.AutofillNudge, userId),
|
||||
this.getNudgeStatus$(NudgeType.AutofillNudge, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff]) => {
|
||||
@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -16,7 +16,7 @@ export class DownloadBitwardenNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Failed to load profile date:");
|
||||
@@ -1,13 +1,15 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service Checking Nudge Status For Empty Vault
|
||||
@@ -20,7 +22,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
organizationService = inject(OrganizationService);
|
||||
collectionService = inject(CollectionService);
|
||||
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
@@ -28,7 +30,10 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
this.collectionService.decryptedCollections$,
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
|
||||
const filteredCiphers = ciphers?.filter((cipher) => {
|
||||
return cipher.deletedDate == null;
|
||||
});
|
||||
const vaultHasContents = !(filteredCiphers == null || filteredCiphers.length === 0);
|
||||
if (orgs == null || orgs.length === 0) {
|
||||
return nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed
|
||||
? of(nudgeStatus)
|
||||
@@ -0,0 +1,67 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, from, Observable, of, switchMap } from "rxjs";
|
||||
import { catchError } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Custom Nudge Service Checking Nudge Status For Welcome Nudge With Populated Vault
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
vaultProfileService = inject(VaultProfileService);
|
||||
logService = inject(LogService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Error getting profile creation date");
|
||||
// Default to today to ensure we show the nudge
|
||||
return of(new Date());
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
profileDate$,
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
]).pipe(
|
||||
switchMap(async ([ciphers, nudgeStatus, profileDate, profileCutoff]) => {
|
||||
const profileOlderThanCutoff = profileDate.getTime() < profileCutoff;
|
||||
const filteredCiphers = ciphers?.filter((cipher) => {
|
||||
return cipher.deletedDate == null;
|
||||
});
|
||||
|
||||
if (profileOlderThanCutoff && filteredCiphers.length > 0) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
};
|
||||
// permanently dismiss both the Empty Vault Nudge and Has Items Vault Nudge if the profile is older than 30 days
|
||||
await this.setNudgeStatus(nudgeType, dismissedStatus, userId);
|
||||
await this.setNudgeStatus(NudgeType.EmptyVaultNudge, dismissedStatus, userId);
|
||||
return dismissedStatus;
|
||||
} else if (nudgeStatus.hasSpotlightDismissed) {
|
||||
return nudgeStatus;
|
||||
} else {
|
||||
return {
|
||||
hasBadgeDismissed: filteredCiphers == null || filteredCiphers.length === 0,
|
||||
hasSpotlightDismissed: filteredCiphers == null || filteredCiphers.length === 0,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./autofill-nudge.service";
|
||||
export * from "./account-security-nudge.service";
|
||||
export * from "./has-items-nudge.service";
|
||||
export * from "./download-bitwarden-nudge.service";
|
||||
export * from "./empty-vault-nudge.service";
|
||||
@@ -6,7 +6,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service Checking Nudge Status For Vault New Item Types
|
||||
@@ -17,7 +17,7 @@ import { NudgeStatus, VaultNudgeType } from "../vault-nudges.service";
|
||||
export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
@@ -30,19 +30,19 @@ export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||
let currentType: CipherType;
|
||||
|
||||
switch (nudgeType) {
|
||||
case VaultNudgeType.newLoginItemStatus:
|
||||
case NudgeType.NewLoginItemStatus:
|
||||
currentType = CipherType.Login;
|
||||
break;
|
||||
case VaultNudgeType.newCardItemStatus:
|
||||
case NudgeType.NewCardItemStatus:
|
||||
currentType = CipherType.Card;
|
||||
break;
|
||||
case VaultNudgeType.newIdentityItemStatus:
|
||||
case NudgeType.NewIdentityItemStatus:
|
||||
currentType = CipherType.Identity;
|
||||
break;
|
||||
case VaultNudgeType.newNoteItemStatus:
|
||||
case NudgeType.NewNoteItemStatus:
|
||||
currentType = CipherType.SecureNote;
|
||||
break;
|
||||
case VaultNudgeType.newSshItemStatus:
|
||||
case NudgeType.NewSshItemStatus:
|
||||
currentType = CipherType.SshKey;
|
||||
break;
|
||||
}
|
||||
@@ -4,19 +4,15 @@ import { map, Observable } from "rxjs";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
NudgeStatus,
|
||||
VAULT_NUDGE_DISMISSED_DISK_KEY,
|
||||
VaultNudgeType,
|
||||
} from "./vault-nudges.service";
|
||||
import { NudgeStatus, NUDGE_DISMISSED_DISK_KEY, NudgeType } from "./nudges.service";
|
||||
|
||||
/**
|
||||
* Base interface for handling a nudge's status
|
||||
*/
|
||||
export interface SingleNudgeService {
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus>;
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus>;
|
||||
|
||||
setNudgeStatus(nudgeType: VaultNudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>;
|
||||
setNudgeStatus(nudgeType: NudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,9 +24,9 @@ export interface SingleNudgeService {
|
||||
export class DefaultSingleNudgeService implements SingleNudgeService {
|
||||
stateProvider = inject(StateProvider);
|
||||
|
||||
protected getNudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
protected getNudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return this.stateProvider
|
||||
.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY)
|
||||
.getUser(userId, NUDGE_DISMISSED_DISK_KEY)
|
||||
.state$.pipe(
|
||||
map(
|
||||
(nudges) =>
|
||||
@@ -39,16 +35,12 @@ export class DefaultSingleNudgeService implements SingleNudgeService {
|
||||
);
|
||||
}
|
||||
|
||||
nudgeStatus$(nudgeType: VaultNudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return this.getNudgeStatus$(nudgeType, userId);
|
||||
}
|
||||
|
||||
async setNudgeStatus(
|
||||
nudgeType: VaultNudgeType,
|
||||
status: NudgeStatus,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, VAULT_NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
|
||||
async setNudgeStatus(nudgeType: NudgeType, status: NudgeStatus, userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, NUDGE_DISMISSED_DISK_KEY).update((nudges) => {
|
||||
nudges ??= {};
|
||||
nudges[nudgeType] = status;
|
||||
return nudges;
|
||||
@@ -2,15 +2,19 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../common/spec";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../../libs/common/spec";
|
||||
|
||||
import {
|
||||
HasItemsNudgeService,
|
||||
@@ -18,7 +22,7 @@ import {
|
||||
DownloadBitwardenNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
|
||||
import { VaultNudgesService, VaultNudgeType } from "./vault-nudges.service";
|
||||
import { NudgesService, NudgeType } from "./nudges.service";
|
||||
|
||||
describe("Vault Nudges Service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
@@ -29,7 +33,7 @@ describe("Vault Nudges Service", () => {
|
||||
getFeatureFlag: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const vaultNudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService];
|
||||
const nudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService];
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
@@ -38,7 +42,7 @@ describe("Vault Nudges Service", () => {
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultNudgesService,
|
||||
provide: NudgesService,
|
||||
},
|
||||
{
|
||||
provide: DefaultSingleNudgeService,
|
||||
@@ -74,6 +78,14 @@ describe("Vault Nudges Service", () => {
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: mock<PinServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: mock<VaultTimeoutSettingsService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -83,13 +95,13 @@ describe("Vault Nudges Service", () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
VaultNudgeType.EmptyVaultNudge,
|
||||
NudgeType.EmptyVaultNudge,
|
||||
{ hasBadgeDismissed: true, hasSpotlightDismissed: true },
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toEqual({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
|
||||
});
|
||||
@@ -98,27 +110,27 @@ describe("Vault Nudges Service", () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
VaultNudgeType.EmptyVaultNudge,
|
||||
NudgeType.EmptyVaultNudge,
|
||||
{ hasBadgeDismissed: false, hasSpotlightDismissed: false },
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.nudgeStatus$(VaultNudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toEqual({ hasBadgeDismissed: false, hasSpotlightDismissed: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("VaultNudgesService", () => {
|
||||
describe("NudgesService", () => {
|
||||
it("should return true, the proper value from the custom nudge service nudgeStatus$", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: { nudgeStatus$: () => of(true) },
|
||||
});
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId),
|
||||
service.showNudgeStatus$(NudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
@@ -128,10 +140,40 @@ describe("Vault Nudges Service", () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: { nudgeStatus$: () => of(false) },
|
||||
});
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudge$(VaultNudgeType.HasVaultItems, "user-id" as UserId),
|
||||
service.showNudgeStatus$(NudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return showNudgeSpotlight$ false if hasSpotLightDismissed is true", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasSpotlightDismissed: true, hasBadgeDismissed: true }),
|
||||
},
|
||||
});
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudgeSpotlight$(NudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return showNudgeBadge$ false when hasBadgeDismissed is true", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasSpotlightDismissed: true, hasBadgeDismissed: true }),
|
||||
},
|
||||
});
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudgeBadge$(NudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
@@ -140,7 +182,7 @@ describe("Vault Nudges Service", () => {
|
||||
|
||||
describe("HasActiveBadges", () => {
|
||||
it("should return true if a nudgeType with hasBadgeDismissed === false", async () => {
|
||||
vaultNudgeServices.forEach((service) => {
|
||||
nudgeServices.forEach((service) => {
|
||||
TestBed.overrideProvider(service, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasBadgeDismissed: false, hasSpotlightDismissed: false }),
|
||||
@@ -148,21 +190,21 @@ describe("Vault Nudges Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId));
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it("should return false if all nudgeTypes have hasBadgeDismissed === true", async () => {
|
||||
vaultNudgeServices.forEach((service) => {
|
||||
nudgeServices.forEach((service) => {
|
||||
TestBed.overrideProvider(service, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasBadgeDismissed: true, hasSpotlightDismissed: false }),
|
||||
},
|
||||
});
|
||||
});
|
||||
const service = testBed.inject(VaultNudgesService);
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId));
|
||||
|
||||
@@ -3,7 +3,7 @@ import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserKeyDefinition, VAULT_NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AutofillNudgeService,
|
||||
DownloadBitwardenNudgeService,
|
||||
NewItemNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
@@ -23,24 +24,29 @@ export type NudgeStatus = {
|
||||
/**
|
||||
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
|
||||
*/
|
||||
export enum VaultNudgeType {
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum NudgeType {
|
||||
/** Nudge to show when user has no items in their vault
|
||||
* Add future nudges here
|
||||
*/
|
||||
EmptyVaultNudge = "empty-vault-nudge",
|
||||
HasVaultItems = "has-vault-items",
|
||||
AutofillNudge = "autofill-nudge",
|
||||
AccountSecurity = "account-security",
|
||||
DownloadBitwarden = "download-bitwarden",
|
||||
newLoginItemStatus = "new-login-item-status",
|
||||
newCardItemStatus = "new-card-item-status",
|
||||
newIdentityItemStatus = "new-identity-item-status",
|
||||
newNoteItemStatus = "new-note-item-status",
|
||||
newSshItemStatus = "new-ssh-item-status",
|
||||
NewLoginItemStatus = "new-login-item-status",
|
||||
NewCardItemStatus = "new-card-item-status",
|
||||
NewIdentityItemStatus = "new-identity-item-status",
|
||||
NewNoteItemStatus = "new-note-item-status",
|
||||
NewSshItemStatus = "new-ssh-item-status",
|
||||
GeneratorNudgeStatus = "generator-nudge-status",
|
||||
SendNudgeStatus = "send-nudge-status",
|
||||
}
|
||||
|
||||
export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||
Partial<Record<VaultNudgeType, NudgeStatus>>
|
||||
>(VAULT_NUDGES_DISK, "vaultNudgeDismissed", {
|
||||
export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||
Partial<Record<NudgeType, NudgeStatus>>
|
||||
>(NUDGES_DISK, "vaultNudgeDismissed", {
|
||||
deserializer: (nudge) => nudge,
|
||||
clearOn: [], // Do not clear dismissals
|
||||
});
|
||||
@@ -48,7 +54,7 @@ export const VAULT_NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultNudgesService {
|
||||
export class NudgesService {
|
||||
private newItemNudgeService = inject(NewItemNudgeService);
|
||||
|
||||
/**
|
||||
@@ -56,16 +62,17 @@ export class VaultNudgesService {
|
||||
* Each nudge type can have its own service to determine when to show the nudge
|
||||
* @private
|
||||
*/
|
||||
private customNudgeServices: Partial<Record<VaultNudgeType, SingleNudgeService>> = {
|
||||
[VaultNudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||
[VaultNudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||
[VaultNudgeType.AutofillNudge]: inject(AutofillNudgeService),
|
||||
[VaultNudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
||||
[VaultNudgeType.newLoginItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newCardItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newIdentityItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newNoteItemStatus]: this.newItemNudgeService,
|
||||
[VaultNudgeType.newSshItemStatus]: this.newItemNudgeService,
|
||||
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
|
||||
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
|
||||
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
|
||||
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
|
||||
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewCardItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewIdentityItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewNoteItemStatus]: this.newItemNudgeService,
|
||||
[NudgeType.NewSshItemStatus]: this.newItemNudgeService,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,16 +83,52 @@ export class VaultNudgesService {
|
||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
private getNudgeService(nudge: VaultNudgeType): SingleNudgeService {
|
||||
private getNudgeService(nudge: NudgeType): SingleNudgeService {
|
||||
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nudge Spotlight should be shown to the user
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeSpotlight$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nudge Badge should be shown to the user
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeBadge$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nudge should be shown to the user
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
showNudge$(nudge: VaultNudgeType, userId: UserId) {
|
||||
showNudgeStatus$(nudge: NudgeType, userId: UserId) {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
@@ -101,7 +144,7 @@ export class VaultNudgesService {
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
async dismissNudge(nudge: VaultNudgeType, userId: UserId, onlyBadge: boolean = false) {
|
||||
async dismissNudge(nudge: NudgeType, userId: UserId, onlyBadge: boolean = false) {
|
||||
const dismissedStatus = onlyBadge
|
||||
? { hasBadgeDismissed: true, hasSpotlightDismissed: false }
|
||||
: { hasBadgeDismissed: true, hasSpotlightDismissed: true };
|
||||
@@ -114,7 +157,7 @@ export class VaultNudgesService {
|
||||
*/
|
||||
hasActiveBadges$(userId: UserId): Observable<boolean> {
|
||||
// Add more nudge types here if they have the settings badge feature
|
||||
const nudgeTypes = [VaultNudgeType.EmptyVaultNudge, VaultNudgeType.DownloadBitwarden];
|
||||
const nudgeTypes = [NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden];
|
||||
|
||||
const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => {
|
||||
return this.getNudgeService(nudge)
|
||||
@@ -2,6 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, from, map, mergeMap, Observable, switchMap, take } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { AnonLayoutComponent } from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Icon, Translation } from "@bitwarden/components";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
Environment,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ButtonModule } from "@bitwarden/components";
|
||||
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@if (initializing) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
} @else {
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[inlineButtons]="true"
|
||||
[primaryButtonText]="{ key: 'changeMasterPassword' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
>
|
||||
</auth-input-password>
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
imports: [InputPasswordComponent, I18nPipe],
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
|
||||
|
||||
activeAccount: Account | null = null;
|
||||
email?: string;
|
||||
userId?: UserId;
|
||||
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
initializing = true;
|
||||
submitting = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private changePasswordService: ChangePasswordService,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = this.activeAccount?.id;
|
||||
this.email = this.activeAccount?.email;
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
this.masterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(this.userId),
|
||||
);
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
try {
|
||||
if (passwordInputResult.rotateUserKey) {
|
||||
if (this.activeAccount == null) {
|
||||
throw new Error("activeAccount not found");
|
||||
}
|
||||
|
||||
if (passwordInputResult.currentPassword == null) {
|
||||
throw new Error("currentPassword not found");
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
passwordInputResult.currentPassword,
|
||||
passwordInputResult.newPassword,
|
||||
this.activeAccount,
|
||||
passwordInputResult.newPasswordHint,
|
||||
);
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PasswordInputResult } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class ChangePasswordService {
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with it.
|
||||
* - does so by calling the underlying method on the `UserKeyRotationService`
|
||||
* - implemented in Web only
|
||||
*
|
||||
* @param currentPassword the current password
|
||||
* @param newPassword the new password
|
||||
* @param user the user account
|
||||
* @param newPasswordHint the new password hint
|
||||
* @throws if called from a non-Web client
|
||||
*/
|
||||
abstract rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
newPasswordHint: string,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Changes the user's password and re-encrypts the user key with the `newMasterKey`.
|
||||
* - Specifically, this method uses credentials from the `passwordInputResult` to:
|
||||
* 1. Decrypt the user key with the `currentMasterKey`
|
||||
* 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey`
|
||||
* 3. Build a `PasswordRequest` object that gets POSTed to `"/accounts/password"`
|
||||
*
|
||||
* @param passwordInputResult credentials object received from the `InputPasswordComponent`
|
||||
* @param userId the `userId`
|
||||
* @throws if the `userId`, `currentMasterKey`, or `currentServerMasterKeyHash` is not found
|
||||
*/
|
||||
abstract changePassword(passwordInputResult: PasswordInputResult, userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
import { DefaultChangePasswordService } from "./default-change-password.service";
|
||||
|
||||
describe("DefaultChangePasswordService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
|
||||
let sut: ChangePasswordService;
|
||||
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const user: Account = {
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
};
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
currentMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
currentServerMasterKeyHash: "currentServerMasterKeyHash",
|
||||
|
||||
newPassword: "newPassword",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
|
||||
kdfConfig: new PBKDF2KdfConfig(),
|
||||
};
|
||||
|
||||
const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const newMasterKeyEncryptedUserKey: [UserKey, EncString] = [
|
||||
decryptedUserKey,
|
||||
{ encryptedString: "newMasterKeyEncryptedUserKey" } as EncString,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
|
||||
sut = new DefaultChangePasswordService(
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
);
|
||||
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(decryptedUserKey);
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(newMasterKeyEncryptedUserKey);
|
||||
});
|
||||
|
||||
describe("changePassword()", () => {
|
||||
it("should call the postPassword() API method with a the correct PasswordRequest credentials", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
masterPasswordHash: passwordInputResult.currentServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
key: newMasterKeyEncryptedUserKey[1].encryptedString,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call decryptUserKeyWithMasterKey and encryptUserKeyWithMasterKey", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a userId was not found", async () => {
|
||||
// Arrange
|
||||
const userId: null = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("userId not found");
|
||||
});
|
||||
|
||||
it("should throw if a currentMasterKey was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentMasterKey = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a currentServerMasterKeyHash was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentServerMasterKeyHash = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if user key decryption fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not decrypt user key");
|
||||
});
|
||||
|
||||
it("should throw an error if postPassword() fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordApiService.postPassword.mockRejectedValueOnce(new Error("error"));
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not change password");
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
|
||||
it("should throw an error (the method is only implemented in Web)", async () => {
|
||||
// Act
|
||||
const testFn = sut.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
user,
|
||||
"newPasswordHint",
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { PasswordInputResult, ChangePasswordService } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export class DefaultChangePasswordService implements ChangePasswordService {
|
||||
constructor(
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
hint: string,
|
||||
): Promise<void> {
|
||||
throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web");
|
||||
}
|
||||
|
||||
async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) {
|
||||
if (!userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
if (!passwordInputResult.currentMasterKey || !passwordInputResult.currentServerMasterKeyHash) {
|
||||
throw new Error("currentMasterKey or currentServerMasterKeyHash not found");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (decryptedUserKey == null) {
|
||||
throw new Error("Could not decrypt user key");
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash;
|
||||
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
|
||||
request.masterPasswordHint = passwordInputResult.newPasswordHint;
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
|
||||
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
} catch {
|
||||
throw new Error("Could not change password");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
|
||||
export type FingerprintDialogData = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const BitwardenLogo = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const BitwardenShield = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const DeviceVerificationIcon = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const DevicesIcon = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const LockIcon = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const RegistrationCheckEmailIcon = svgIcon`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const RegistrationExpiredLinkIcon = svgIcon`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user