mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 14:04:03 +00:00
Merge branch 'main' into platform/pm-9388/add-duckduckgo-browser-device-type
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ButtonModule } from "@bitwarden/components";
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-authentication-timeout",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, ButtonModule, RouterModule],
|
||||
template: `
|
||||
<p class="tw-text-center">
|
||||
|
||||
@@ -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,13 +5,15 @@ 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";
|
||||
|
||||
import { activeAuthGuard } from "./active-auth.guard";
|
||||
|
||||
@Component({ template: "" })
|
||||
@Component({ template: "", standalone: false })
|
||||
class EmptyComponent {}
|
||||
|
||||
describe("activeAuthGuard", () => {
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("lockGuard", () => {
|
||||
|
||||
const keyService: MockProxy<KeyService> = mock<KeyService>();
|
||||
keyService.isLegacyUser.mockResolvedValue(setupParams.isLegacyUser);
|
||||
keyService.everHadUserKey$ = of(setupParams.everHadUserKey);
|
||||
keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey));
|
||||
|
||||
const platformUtilService: MockProxy<PlatformUtilsService> = mock<PlatformUtilsService>();
|
||||
platformUtilService.getClientType.mockReturnValue(setupParams.clientType);
|
||||
@@ -79,7 +79,6 @@ describe("lockGuard", () => {
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "lock", component: EmptyComponent, canActivate: [lockGuard()] },
|
||||
{ path: "non-lock-route", component: EmptyComponent },
|
||||
{ path: "migrate-legacy-encryption", component: EmptyComponent },
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -182,18 +181,6 @@ describe("lockGuard", () => {
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should send the user to migrate-legacy-encryption if they are a legacy user on a web client", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Web,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/migrate-legacy-encryption");
|
||||
});
|
||||
|
||||
it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
|
||||
@@ -11,11 +11,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
/**
|
||||
@@ -33,7 +31,6 @@ export function lockGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const platformUtilService = inject(PlatformUtilsService);
|
||||
const messagingService = inject(MessagingService);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
@@ -59,12 +56,7 @@ export function lockGuard(): CanActivateFn {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If legacy user on web, redirect to migration page
|
||||
if (await keyService.isLegacyUser()) {
|
||||
if (platformUtilService.getClientType() === ClientType.Web) {
|
||||
return router.createUrlTree(["migrate-legacy-encryption"]);
|
||||
}
|
||||
// Log out legacy users on other clients
|
||||
messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
@@ -84,7 +76,7 @@ export function lockGuard(): CanActivateFn {
|
||||
}
|
||||
|
||||
// If authN user with TDE directly navigates to lock, reject that navigation
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(activeUser.id));
|
||||
if (tdeEnabled && !everHadUserKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -33,6 +35,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const accountService = inject(AccountService);
|
||||
const logService = inject(LogService);
|
||||
const router = inject(Router);
|
||||
|
||||
@@ -49,7 +52,8 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
|
||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||
// login decryption options component.
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$);
|
||||
const userId = await firstValueFrom(accountService.activeAccount$.pipe(getUserId));
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(userId));
|
||||
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
|
||||
logService.info(
|
||||
"Sending user to TDE decryption options. AuthStatus is %s. TDE support is %s. Ever had user key is %s.",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router, provideRouter } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { tdeDecryptionRequiredGuard } from "./tde-decryption-required.guard";
|
||||
|
||||
describe("tdeDecryptionRequiredGuard", () => {
|
||||
const activeUser: Account = {
|
||||
id: "fake_user_id" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
};
|
||||
|
||||
const setup = (
|
||||
activeUser: Account | null,
|
||||
authStatus: AuthenticationStatus | null = null,
|
||||
tdeEnabled: boolean = false,
|
||||
everHadUserKey: boolean = false,
|
||||
) => {
|
||||
const accountService = mock<AccountService>();
|
||||
const authService = mock<AuthService>();
|
||||
const keyService = mock<KeyService>();
|
||||
const deviceTrustService = mock<DeviceTrustServiceAbstraction>();
|
||||
const logService = mock<LogService>();
|
||||
|
||||
accountService.activeAccount$ = new BehaviorSubject<Account | null>(activeUser);
|
||||
if (authStatus !== null) {
|
||||
authService.getAuthStatus.mockResolvedValue(authStatus);
|
||||
}
|
||||
keyService.everHadUserKey$.mockReturnValue(of(everHadUserKey));
|
||||
deviceTrustService.supportsDeviceTrust$ = of(tdeEnabled);
|
||||
|
||||
const testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: DeviceTrustServiceAbstraction, useValue: deviceTrustService },
|
||||
{ provide: LogService, useValue: logService },
|
||||
provideRouter([
|
||||
{ path: "", component: EmptyComponent },
|
||||
{
|
||||
path: "protected-route",
|
||||
component: EmptyComponent,
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
router: testBed.inject(Router),
|
||||
};
|
||||
};
|
||||
|
||||
it("redirects to root when the active account is null", async () => {
|
||||
const { router } = setup(null, null);
|
||||
await router.navigate(["protected-route"]);
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
test.each([AuthenticationStatus.Unlocked, AuthenticationStatus.LoggedOut])(
|
||||
"redirects to root when the user isn't locked",
|
||||
async (authStatus) => {
|
||||
const { router } = setup(activeUser, authStatus);
|
||||
|
||||
await router.navigate(["protected-route"]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
},
|
||||
);
|
||||
|
||||
it("redirects to root when TDE is not enabled", async () => {
|
||||
const { router } = setup(activeUser, AuthenticationStatus.Locked, false, true);
|
||||
|
||||
await router.navigate(["protected-route"]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("redirects to root when user has had a user key", async () => {
|
||||
const { router } = setup(activeUser, AuthenticationStatus.Locked, true, true);
|
||||
|
||||
await router.navigate(["protected-route"]);
|
||||
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("allows access when user is locked, TDE is enabled, and user has never had a user key", async () => {
|
||||
const { router } = setup(activeUser, AuthenticationStatus.Locked, true, false);
|
||||
|
||||
const result = await router.navigate(["protected-route"]);
|
||||
expect(result).toBe(true);
|
||||
expect(router.url).toBe("/protected-route");
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,9 @@ import {
|
||||
RouterStateSnapshot,
|
||||
CanActivateFn,
|
||||
} from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
@@ -24,12 +25,18 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const accountService = inject(AccountService);
|
||||
const logService = inject(LogService);
|
||||
const router = inject(Router);
|
||||
|
||||
const userId = await firstValueFrom(accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
if (userId == null) {
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(userId));
|
||||
|
||||
// We need to determine if we should bypass the decryption options and send the user to the vault.
|
||||
// The ONLY time that we want to send a user to the decryption options is when:
|
||||
|
||||
@@ -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 { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -43,7 +43,7 @@ describe("UnauthGuard", () => {
|
||||
authService.authStatusFor$.mockReturnValue(activeAccountStatusObservable);
|
||||
}
|
||||
|
||||
keyService.everHadUserKey$ = new BehaviorSubject<boolean>(everHadUserKey);
|
||||
keyService.everHadUserKey$.mockReturnValue(of(everHadUserKey));
|
||||
deviceTrustService.supportsDeviceTrustByUserId$.mockReturnValue(
|
||||
new BehaviorSubject<boolean>(tdeEnabled),
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ async function unauthGuard(
|
||||
const tdeEnabled = await firstValueFrom(
|
||||
deviceTrustService.supportsDeviceTrustByUserId$(activeUser.id),
|
||||
);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$);
|
||||
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(activeUser.id));
|
||||
|
||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||
// login decryption options component.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,6 +27,7 @@ const testStringFeatureValue = "test-value";
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
class TestComponent {
|
||||
testBooleanFeature = testBooleanFeature;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appTextDrag]",
|
||||
standalone: true,
|
||||
host: {
|
||||
draggable: "true",
|
||||
class: "tw-cursor-move",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
@Pipe({
|
||||
name: "pluralize",
|
||||
standalone: true,
|
||||
})
|
||||
export class PluralizePipe implements PipeTransform {
|
||||
transform(count: number, singular: string, plural: string): string {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { I18nMockService, ToastService } from "@bitwarden/components/src";
|
||||
|
||||
import { canAccessFeature } from "./feature-flag.guard";
|
||||
|
||||
@Component({ template: "" })
|
||||
@Component({ template: "", standalone: false })
|
||||
export class EmptyComponent {}
|
||||
|
||||
describe("canAccessFeature", () => {
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
124
libs/angular/src/platform/view-cache/README.md
Normal file
124
libs/angular/src/platform/view-cache/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 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]
|
||||
> 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]
|
||||
> 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";
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message } from "@bitwarden/common/platform/messaging";
|
||||
import { HttpOperations } from "@bitwarden/common/services/api.service";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// Re-export the SafeInjectionToken from ui-common
|
||||
export { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
@@ -61,3 +64,5 @@ export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() =>
|
||||
export const ENV_ADDITIONAL_REGIONS = new SafeInjectionToken<RegionConfig[]>(
|
||||
"ENV_ADDITIONAL_REGIONS",
|
||||
);
|
||||
|
||||
export const HTTP_OPERATIONS = new SafeInjectionToken<HttpOperations>("HTTP_OPERATIONS");
|
||||
|
||||
@@ -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,18 +337,20 @@ 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,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
ENV_ADDITIONAL_REGIONS,
|
||||
HTTP_OPERATIONS,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
LOCALES_DIRECTORY,
|
||||
LOCKED_CALLBACK,
|
||||
@@ -507,6 +521,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateProvider: StateProvider,
|
||||
accountService: AccountServiceAbstraction,
|
||||
logService: LogService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
) =>
|
||||
new CipherService(
|
||||
keyService,
|
||||
@@ -523,6 +538,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateProvider,
|
||||
accountService,
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
),
|
||||
deps: [
|
||||
KeyService,
|
||||
@@ -539,6 +555,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
AccountServiceAbstraction,
|
||||
LogService,
|
||||
CipherEncryptionService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -700,6 +717,10 @@ const safeProviders: SafeProvider[] = [
|
||||
},
|
||||
deps: [ToastService, I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: HTTP_OPERATIONS,
|
||||
useValue: { createRequest: (url, request) => new Request(url, request) },
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ApiServiceAbstraction,
|
||||
useClass: ApiService,
|
||||
@@ -712,6 +733,7 @@ const safeProviders: SafeProvider[] = [
|
||||
LogService,
|
||||
LOGOUT_CALLBACK,
|
||||
VaultTimeoutSettingsService,
|
||||
HTTP_OPERATIONS,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1072,7 +1094,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: OrganizationSponsorshipApiServiceAbstraction,
|
||||
useClass: OrganizationSponsorshipApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
deps: [ApiServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationBillingApiServiceAbstraction,
|
||||
@@ -1489,7 +1511,6 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
ApiServiceAbstraction,
|
||||
OrganizationServiceAbstraction,
|
||||
ConfigService,
|
||||
AuthServiceAbstraction,
|
||||
NotificationsService,
|
||||
MessageListener,
|
||||
@@ -1521,6 +1542,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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@ type BackgroundTypes = "danger" | "primary" | "success" | "warning";
|
||||
@Component({
|
||||
selector: "tools-password-strength",
|
||||
templateUrl: "password-strength-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, ProgressModule],
|
||||
})
|
||||
export class PasswordStrengthV2Component implements OnChanges {
|
||||
|
||||
@@ -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";
|
||||
@@ -24,11 +26,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
CipherService,
|
||||
EncryptionContext,
|
||||
} from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
@@ -40,6 +44,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 +275,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) {
|
||||
@@ -738,17 +742,17 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return this.cipherService.encrypt(this.cipher, userId);
|
||||
}
|
||||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
protected saveCipher(data: EncryptionContext) {
|
||||
let orgAdmin = this.organization?.canEditAllCiphers;
|
||||
|
||||
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
|
||||
if (!cipher.collectionIds) {
|
||||
if (!data.cipher.collectionIds) {
|
||||
orgAdmin = this.organization?.canEditUnassignedCiphers;
|
||||
}
|
||||
|
||||
return this.cipher.id == null
|
||||
? this.cipherService.createWithServer(cipher, orgAdmin)
|
||||
: this.cipherService.updateWithServer(cipher, orgAdmin);
|
||||
? this.cipherService.createWithServer(data, orgAdmin)
|
||||
: this.cipherService.updateWithServer(data, orgAdmin);
|
||||
}
|
||||
|
||||
protected deleteCipher(userId: UserId) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<div
|
||||
class="tw-rounded-2xl tw-bg-primary-100 tw-border-primary-600 tw-border-solid tw-border tw-p-4 tw-pt-3 tw-flex tw-flex-col tw-gap-2"
|
||||
>
|
||||
<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
|
||||
*ngIf="subtitle"
|
||||
class="tw-text-main tw-mb-0"
|
||||
bitTypography="body2"
|
||||
[innerHTML]="subtitle"
|
||||
></p>
|
||||
<ng-content *ngIf="!subtitle"></ng-content>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
size="small"
|
||||
*ngIf="!persistent"
|
||||
(click)="handleDismiss()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="tw-w-full"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
*ngIf="buttonText"
|
||||
(click)="handleButtonClick($event)"
|
||||
>
|
||||
{{ buttonText }}
|
||||
<i *ngIf="buttonIcon" [ngClass]="buttonIcon" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "bit-spotlight",
|
||||
templateUrl: "spotlight.component.html",
|
||||
imports: [ButtonModule, CommonModule, IconButtonModule, I18nPipe, TypographyModule],
|
||||
})
|
||||
export class SpotlightComponent {
|
||||
// The title of the component
|
||||
@Input({ required: true }) title: string | null = null;
|
||||
// The subtitle of the component
|
||||
@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
|
||||
@Input() persistent = false;
|
||||
// Optional icon to display on the button
|
||||
@Input() buttonIcon: string | null = null;
|
||||
@Output() onDismiss = new EventEmitter<void>();
|
||||
@Output() onButtonClick = new EventEmitter();
|
||||
|
||||
handleButtonClick(event: MouseEvent): void {
|
||||
this.onButtonClick.emit(event);
|
||||
}
|
||||
|
||||
handleDismiss(): void {
|
||||
this.onDismiss.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { moduleMetadata, Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
I18nMockService,
|
||||
IconButtonModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SpotlightComponent } from "./spotlight.component";
|
||||
|
||||
const meta: Meta<SpotlightComponent> = {
|
||||
title: "Vault/Spotlight",
|
||||
component: SpotlightComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule, IconButtonModule, TypographyModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
title: "Primary",
|
||||
subtitle: "Callout Text",
|
||||
buttonText: "Button",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<SpotlightComponent>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithoutButton: Story = {
|
||||
args: {
|
||||
buttonText: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const Persistent: Story = {
|
||||
args: {
|
||||
persistent: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithButtonIcon: Story = {
|
||||
args: {
|
||||
buttonIcon: "bwi bwi-external-link",
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-spotlight
|
||||
[title]="title"
|
||||
[subtitle]="subtitle"
|
||||
buttonText="External Link"
|
||||
buttonIcon="bwi-external-link"
|
||||
></bit-spotlight>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, combineLatest, from, map, of } 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 { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Custom Nudge Service to use for the Autofill Nudge in the Vault
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class AutofillNudgeService extends DefaultSingleNudgeService {
|
||||
vaultProfileService = inject(VaultProfileService);
|
||||
logService = inject(LogService);
|
||||
|
||||
nudgeStatus$(_: 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([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(NudgeType.AutofillNudge, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || profileOlderThanCutoff,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || profileOlderThanCutoff,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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";
|
||||
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 DownloadBitwardenNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
|
||||
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
|
||||
return of(new Date());
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || profileOlderThanCutoff,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || profileOlderThanCutoff,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service Checking Nudge Status For Empty Vault
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
organizationService = inject(OrganizationService);
|
||||
collectionService = inject(CollectionService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
|
||||
if (orgs == null || orgs.length === 0) {
|
||||
return nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed
|
||||
? of(nudgeStatus)
|
||||
: of({
|
||||
hasSpotlightDismissed: vaultHasContents,
|
||||
hasBadgeDismissed: vaultHasContents,
|
||||
});
|
||||
}
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId),
|
||||
);
|
||||
// Do not show nudge when
|
||||
// user has previously dismissed nudge
|
||||
// OR
|
||||
// user belongs to an organization and cannot create collections || manage collections
|
||||
if (
|
||||
nudgeStatus.hasBadgeDismissed ||
|
||||
nudgeStatus.hasSpotlightDismissed ||
|
||||
hasManageCollections ||
|
||||
canCreateCollections
|
||||
) {
|
||||
return of(nudgeStatus);
|
||||
}
|
||||
return of({
|
||||
hasSpotlightDismissed: vaultHasContents,
|
||||
hasBadgeDismissed: vaultHasContents,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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";
|
||||
export * from "./vault-settings-import-nudge.service";
|
||||
export * from "./new-item-nudge.service";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service Checking Nudge Status For Vault New Item Types
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
]).pipe(
|
||||
switchMap(async ([nudgeStatus, ciphers]) => {
|
||||
if (nudgeStatus.hasSpotlightDismissed) {
|
||||
return nudgeStatus;
|
||||
}
|
||||
|
||||
let currentType: CipherType;
|
||||
|
||||
switch (nudgeType) {
|
||||
case NudgeType.NewLoginItemStatus:
|
||||
currentType = CipherType.Login;
|
||||
break;
|
||||
case NudgeType.NewCardItemStatus:
|
||||
currentType = CipherType.Card;
|
||||
break;
|
||||
case NudgeType.NewIdentityItemStatus:
|
||||
currentType = CipherType.Identity;
|
||||
break;
|
||||
case NudgeType.NewNoteItemStatus:
|
||||
currentType = CipherType.SecureNote;
|
||||
break;
|
||||
case NudgeType.NewSshItemStatus:
|
||||
currentType = CipherType.SshKey;
|
||||
break;
|
||||
}
|
||||
|
||||
const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType);
|
||||
|
||||
if (ciphersBoolean) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
};
|
||||
await this.setNudgeStatus(nudgeType, dismissedStatus, userId);
|
||||
return dismissedStatus;
|
||||
}
|
||||
|
||||
return nudgeStatus;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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, NudgeType } from "../nudges.service";
|
||||
|
||||
/**
|
||||
* Custom Nudge Service for the vault settings import badge.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
cipherService = inject(CipherService);
|
||||
organizationService = inject(OrganizationService);
|
||||
collectionService = inject(CollectionService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return combineLatest([
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
|
||||
const { hasBadgeDismissed, hasSpotlightDismissed } = nudgeStatus;
|
||||
|
||||
// When the user has no organizations, return the nudge status directly
|
||||
if ((orgs?.length ?? 0) === 0) {
|
||||
return hasBadgeDismissed || hasSpotlightDismissed
|
||||
? of(nudgeStatus)
|
||||
: of({
|
||||
hasSpotlightDismissed: vaultHasMoreThanOneItem,
|
||||
hasBadgeDismissed: vaultHasMoreThanOneItem,
|
||||
});
|
||||
}
|
||||
|
||||
const orgIds = new Set(orgs.map((org) => org.id));
|
||||
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
|
||||
const hasManageCollections = collections.some(
|
||||
(c) => c.manage && orgIds.has(c.organizationId),
|
||||
);
|
||||
|
||||
// When the user has dismissed the nudge or spotlight, return the nudge status directly
|
||||
if (hasBadgeDismissed || hasSpotlightDismissed) {
|
||||
return of(nudgeStatus);
|
||||
}
|
||||
|
||||
// When the user belongs to an organization and cannot create collections or manage collections,
|
||||
// hide the nudge and spotlight
|
||||
if (!hasManageCollections && !canCreateCollections) {
|
||||
return of({
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, return the nudge status based on the vault contents
|
||||
return of({
|
||||
hasSpotlightDismissed: vaultHasMoreThanOneItem,
|
||||
hasBadgeDismissed: vaultHasMoreThanOneItem,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { NudgeStatus, NUDGE_DISMISSED_DISK_KEY, NudgeType } from "./nudges.service";
|
||||
|
||||
/**
|
||||
* Base interface for handling a nudge's status
|
||||
*/
|
||||
export interface SingleNudgeService {
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus>;
|
||||
|
||||
setNudgeStatus(nudgeType: NudgeType, newStatus: NudgeStatus, userId: UserId): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for nudges. Set and Show Nudge dismissed state
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class DefaultSingleNudgeService implements SingleNudgeService {
|
||||
stateProvider = inject(StateProvider);
|
||||
|
||||
protected getNudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return this.stateProvider
|
||||
.getUser(userId, NUDGE_DISMISSED_DISK_KEY)
|
||||
.state$.pipe(
|
||||
map(
|
||||
(nudges) =>
|
||||
nudges?.[nudgeType] ?? { hasBadgeDismissed: false, hasSpotlightDismissed: false },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
return this.getNudgeStatus$(nudgeType, userId);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
219
libs/angular/src/vault/services/nudges.service.spec.ts
Normal file
219
libs/angular/src/vault/services/nudges.service.spec.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
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 "../../../../../libs/common/spec";
|
||||
|
||||
import {
|
||||
HasItemsNudgeService,
|
||||
EmptyVaultNudgeService,
|
||||
DownloadBitwardenNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
|
||||
import { NudgesService, NudgeType } from "./nudges.service";
|
||||
|
||||
describe("Vault Nudges Service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
let testBed: TestBed;
|
||||
const mockConfigService = {
|
||||
getFeatureFlag$: jest.fn().mockReturnValue(of(true)),
|
||||
getFeatureFlag: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const nudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService];
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: NudgesService,
|
||||
},
|
||||
{
|
||||
provide: DefaultSingleNudgeService,
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{
|
||||
provide: HasItemsNudgeService,
|
||||
useValue: mock<HasItemsNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: DownloadBitwardenNudgeService,
|
||||
useValue: mock<DownloadBitwardenNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: EmptyVaultNudgeService,
|
||||
useValue: mock<EmptyVaultNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultSettingsImportNudgeService,
|
||||
useValue: mock<VaultSettingsImportNudgeService>(),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mock<AccountService>(),
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: mock<PinServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: mock<VaultTimeoutSettingsService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("DefaultSingleNudgeService", () => {
|
||||
it("should return hasSpotlightDismissed === true when EmptyVaultNudge dismissed is true", async () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
NudgeType.EmptyVaultNudge,
|
||||
{ hasBadgeDismissed: true, hasSpotlightDismissed: true },
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toEqual({ hasBadgeDismissed: true, hasSpotlightDismissed: true });
|
||||
});
|
||||
|
||||
it("should return hasSpotlightDismissed === true when EmptyVaultNudge dismissed is false", async () => {
|
||||
const service = testBed.inject(DefaultSingleNudgeService);
|
||||
|
||||
await service.setNudgeStatus(
|
||||
NudgeType.EmptyVaultNudge,
|
||||
{ hasBadgeDismissed: false, hasSpotlightDismissed: false },
|
||||
"user-id" as UserId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.nudgeStatus$(NudgeType.EmptyVaultNudge, "user-id" as UserId),
|
||||
);
|
||||
expect(result).toEqual({ hasBadgeDismissed: false, hasSpotlightDismissed: false });
|
||||
});
|
||||
});
|
||||
|
||||
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(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.showNudgeStatus$(NudgeType.HasVaultItems, "user-id" as UserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false, the proper value for the custom nudge service nudgeStatus$", async () => {
|
||||
TestBed.overrideProvider(HasItemsNudgeService, {
|
||||
useValue: { nudgeStatus$: () => of(false) },
|
||||
});
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HasActiveBadges", () => {
|
||||
it("should return true if a nudgeType with hasBadgeDismissed === false", async () => {
|
||||
nudgeServices.forEach((service) => {
|
||||
TestBed.overrideProvider(service, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasBadgeDismissed: false, hasSpotlightDismissed: false }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
nudgeServices.forEach((service) => {
|
||||
TestBed.overrideProvider(service, {
|
||||
useValue: {
|
||||
nudgeStatus$: () => of({ hasBadgeDismissed: true, hasSpotlightDismissed: false }),
|
||||
},
|
||||
});
|
||||
});
|
||||
const service = testBed.inject(NudgesService);
|
||||
|
||||
const result = await firstValueFrom(service.hasActiveBadges$("user-id" as UserId));
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
177
libs/angular/src/vault/services/nudges.service.ts
Normal file
177
libs/angular/src/vault/services/nudges.service.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
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, NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
HasItemsNudgeService,
|
||||
EmptyVaultNudgeService,
|
||||
AutofillNudgeService,
|
||||
DownloadBitwardenNudgeService,
|
||||
NewItemNudgeService,
|
||||
AccountSecurityNudgeService,
|
||||
VaultSettingsImportNudgeService,
|
||||
} from "./custom-nudges-services";
|
||||
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
|
||||
|
||||
export type NudgeStatus = {
|
||||
hasBadgeDismissed: boolean;
|
||||
hasSpotlightDismissed: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
|
||||
*/
|
||||
// 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",
|
||||
VaultSettingsImportNudge = "vault-settings-import-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",
|
||||
GeneratorNudgeStatus = "generator-nudge-status",
|
||||
}
|
||||
|
||||
export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
|
||||
Partial<Record<NudgeType, NudgeStatus>>
|
||||
>(NUDGES_DISK, "vaultNudgeDismissed", {
|
||||
deserializer: (nudge) => nudge,
|
||||
clearOn: [], // Do not clear dismissals
|
||||
});
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class NudgesService {
|
||||
private newItemNudgeService = inject(NewItemNudgeService);
|
||||
|
||||
/**
|
||||
* Custom nudge services to use for specific nudge types
|
||||
* Each nudge type can have its own service to determine when to show the nudge
|
||||
* @private
|
||||
*/
|
||||
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
|
||||
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
|
||||
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
|
||||
[NudgeType.VaultSettingsImportNudge]: inject(VaultSettingsImportNudgeService),
|
||||
[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,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default nudge service to use when no custom service is available
|
||||
* Simply stores the dismissed state in the user's state
|
||||
* @private
|
||||
*/
|
||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
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
|
||||
*/
|
||||
showNudgeStatus$(nudge: NudgeType, userId: UserId) {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true } as NudgeStatus);
|
||||
}
|
||||
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a nudge for the user so that it is not shown again
|
||||
* @param nudge
|
||||
* @param userId
|
||||
*/
|
||||
async dismissNudge(nudge: NudgeType, userId: UserId, onlyBadge: boolean = false) {
|
||||
const dismissedStatus = onlyBadge
|
||||
? { hasBadgeDismissed: true, hasSpotlightDismissed: false }
|
||||
: { hasBadgeDismissed: true, hasSpotlightDismissed: true };
|
||||
await this.getNudgeService(nudge).setNudgeStatus(nudge, dismissedStatus, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any active badges for the user to show Berry notification in Tabs
|
||||
* @param userId
|
||||
*/
|
||||
hasActiveBadges$(userId: UserId): Observable<boolean> {
|
||||
// Add more nudge types here if they have the settings badge feature
|
||||
const nudgeTypes = [NudgeType.EmptyVaultNudge, NudgeType.DownloadBitwarden];
|
||||
|
||||
const nudgeTypesWithBadge$ = nudgeTypes.map((nudge) => {
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(
|
||||
map((status) => !status?.hasBadgeDismissed),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
return combineLatest(nudgeTypesWithBadge$).pipe(
|
||||
map((results) => results.some((result) => result === true)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user