diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml
index 23f4bd35f1..f924c5c98e 100644
--- a/.github/workflows/build-browser.yml
+++ b/.github/workflows/build-browser.yml
@@ -164,6 +164,10 @@ jobs:
run: npm run dist:mv3
working-directory: browser-source/apps/browser
+ - name: Build Chrome Manifest v3 Beta
+ run: npm run dist:chrome:beta
+ working-directory: browser-source/apps/browser
+
- name: Gulp
run: gulp ci
working-directory: browser-source/apps/browser
@@ -196,6 +200,13 @@ jobs:
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
if-no-files-found: error
+ - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
+ uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
+ with:
+ name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
+ path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
+ if-no-files-found: error
+
- name: Upload Firefox artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml
index 6a5d9f1405..b034136f58 100644
--- a/.github/workflows/deploy-web.yml
+++ b/.github/workflows/deploy-web.yml
@@ -230,6 +230,17 @@ jobs:
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ update-summary:
+ name: Display commit
+ needs: artifact-check
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Display commit SHA
+ run: |
+ REPO_URL="https://github.com/bitwarden/clients/commit"
+ COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}"
+ echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY
+
azure-deploy:
name: Deploy Web Vault to ${{ inputs.environment }} Storage Account
needs:
diff --git a/.storybook/main.ts b/.storybook/main.ts
index c71a74c2a7..cb63ada550 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -9,6 +9,8 @@ const config: StorybookConfig = {
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
"../apps/web/src/**/*.mdx",
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
+ "../apps/browser/src/**/*.mdx",
+ "../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)",
"../bitwarden_license/bit-web/src/**/*.mdx",
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
],
diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json
index 113cc5bcde..34acc9a740 100644
--- a/.storybook/tsconfig.json
+++ b/.storybook/tsconfig.json
@@ -1,12 +1,10 @@
{
"extends": "../tsconfig",
"compilerOptions": {
- "types": ["node", "jest", "chrome"],
"allowSyntheticDefaultImports": true
},
- "exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],
+ "exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"],
"files": [
- "./typings.d.ts",
"./preview.tsx",
"../libs/components/src/main.ts",
"../libs/components/src/polyfills.ts"
diff --git a/.storybook/typings.d.ts b/.storybook/typings.d.ts
deleted file mode 100644
index c94d67b1a2..0000000000
--- a/.storybook/typings.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module "*.md" {
- const content: string;
- export default content;
-}
diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js
index 6a0980fc27..3fe2c44dd1 100644
--- a/apps/browser/gulpfile.js
+++ b/apps/browser/gulpfile.js
@@ -30,11 +30,27 @@ const filters = {
safari: ["!build/safari/**/*"],
};
+/**
+ * Converts a number to a tuple containing two Uint16's
+ * @param num {number} This number is expected to be a integer style number with no decimals
+ *
+ * @returns {number[]} A tuple containing two elements that are both numbers.
+ */
+function numToUint16s(num) {
+ var arr = new ArrayBuffer(4);
+ var view = new DataView(arr);
+ view.setUint32(0, num, false);
+ return [view.getUint16(0), view.getUint16(2)];
+}
+
function buildString() {
var build = "";
if (process.env.MANIFEST_VERSION) {
build = `-mv${process.env.MANIFEST_VERSION}`;
}
+ if (process.env.BETA_BUILD === "1") {
+ build += "-beta";
+ }
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`;
}
@@ -65,6 +81,9 @@ function distFirefox() {
manifest.optional_permissions = manifest.optional_permissions.filter(
(permission) => permission !== "privacy",
);
+ if (process.env.BETA_BUILD === "1") {
+ manifest = applyBetaLabels(manifest);
+ }
return manifest;
});
}
@@ -72,6 +91,9 @@ function distFirefox() {
function distOpera() {
return dist("opera", (manifest) => {
delete manifest.applications;
+ if (process.env.BETA_BUILD === "1") {
+ manifest = applyBetaLabels(manifest);
+ }
return manifest;
});
}
@@ -81,6 +103,9 @@ function distChrome() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
+ if (process.env.BETA_BUILD === "1") {
+ manifest = applyBetaLabels(manifest);
+ }
return manifest;
});
}
@@ -90,6 +115,9 @@ function distEdge() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
+ if (process.env.BETA_BUILD === "1") {
+ manifest = applyBetaLabels(manifest);
+ }
return manifest;
});
}
@@ -210,6 +238,9 @@ async function safariCopyBuild(source, dest) {
delete manifest.commands._execute_sidebar_action;
delete manifest.optional_permissions;
manifest.permissions.push("nativeMessaging");
+ if (process.env.BETA_BUILD === "1") {
+ manifest = applyBetaLabels(manifest);
+ }
return manifest;
}),
),
@@ -235,6 +266,30 @@ async function ciCoverage(cb) {
.pipe(gulp.dest(paths.coverage));
}
+function applyBetaLabels(manifest) {
+ manifest.name = "Bitwarden Password Manager BETA";
+ manifest.short_name = "Bitwarden BETA";
+ manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN.";
+ if (process.env.GITHUB_RUN_ID) {
+ const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0
+
+ // GITHUB_RUN_ID is a number like: 8853654662
+ // which will convert to [ 4024, 3206 ]
+ // and a single incremented id of 8853654663 will become [ 4024, 3207 ]
+ const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID));
+
+ // Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID
+ // Example: 2024.4.4024.3206
+ const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`;
+
+ manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`;
+ manifest.version = betaVersion;
+ } else {
+ manifest.version = `${manifest.version}.0`;
+ }
+ return manifest;
+}
+
exports["dist:firefox"] = distFirefox;
exports["dist:chrome"] = distChrome;
exports["dist:opera"] = distOpera;
diff --git a/apps/browser/package.json b/apps/browser/package.json
index 506f19f279..580acfc3d0 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -7,10 +7,14 @@
"build:watch": "webpack --watch",
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
+ "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"dist": "npm run build:prod && gulp dist",
+ "dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist",
"dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist",
+ "dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist",
"dist:chrome": "npm run build:prod && gulp dist:chrome",
+ "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome",
"dist:firefox": "npm run build:prod && gulp dist:firefox",
"dist:opera": "npm run build:prod && gulp dist:opera",
"dist:safari": "npm run build:prod && gulp dist:safari",
diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json
index d3d9106c15..1a56d32a35 100644
--- a/apps/browser/src/_locales/pl/messages.json
+++ b/apps/browser/src/_locales/pl/messages.json
@@ -7,7 +7,7 @@
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
- "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
+ "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {
diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json
index 417bc977eb..0f40bc63bb 100644
--- a/apps/browser/src/_locales/pt_BR/messages.json
+++ b/apps/browser/src/_locales/pt_BR/messages.json
@@ -7,7 +7,7 @@
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
- "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
+ "message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {
diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json
index 4eba4ffaea..6e530412db 100644
--- a/apps/browser/src/_locales/vi/messages.json
+++ b/apps/browser/src/_locales/vi/messages.json
@@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
- "message": "Bitwarden Password Manager",
+ "message": "Bitwarden - Trình Quản lý Mật khẩu",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
- "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
+ "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {
@@ -650,7 +650,7 @@
"message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên."
},
"addLoginNotificationDescAlt": {
- "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts."
+ "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"showCardsCurrentTab": {
"message": "Hiển thị thẻ trên trang Tab"
@@ -685,13 +685,13 @@
"message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web."
},
"changedPasswordNotificationDescAlt": {
- "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts."
+ "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"enableUsePasskeys": {
- "message": "Ask to save and use passkeys"
+ "message": "Đưa ra lựa chọn để lưu và sử dụng passkey"
},
"usePasskeysDesc": {
- "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts."
+ "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"notificationChangeDesc": {
"message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?"
@@ -712,7 +712,7 @@
"message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. "
},
"contextMenuItemDescAlt": {
- "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts."
+ "message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"defaultUriMatchDetection": {
"message": "Phương thức kiểm tra URI mặc định",
@@ -728,7 +728,7 @@
"message": "Thay đổi màu sắc ứng dụng."
},
"themeDescAlt": {
- "message": "Change the application's color theme. Applies to all logged in accounts."
+ "message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"dark": {
"message": "Tối",
@@ -1061,10 +1061,10 @@
"message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột."
},
"turnOffBrowserBuiltInPasswordManagerSettingsLink": {
- "message": "Edit browser settings."
+ "message": "Thay đổi cài đặt của trình duyệt."
},
"autofillOverlayVisibilityOff": {
- "message": "Off",
+ "message": "Tắt",
"description": "Overlay setting select option for disabling autofill overlay"
},
"autofillOverlayVisibilityOnFieldFocus": {
@@ -1168,7 +1168,7 @@
"message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập."
},
"faviconDescAlt": {
- "message": "Show a recognizable image next to each login. Applies to all logged in accounts."
+ "message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
},
"enableBadgeCounter": {
"message": "Hiển thị biểu tượng bộ đếm"
@@ -1500,7 +1500,7 @@
"message": "Mã PIN không hợp lệ."
},
"tooManyInvalidPinEntryAttemptsLoggingOut": {
- "message": "Too many invalid PIN entry attempts. Logging out."
+ "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất."
},
"unlockWithBiometrics": {
"message": "Mở khóa bằng sinh trắc học"
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
index aebf2219ff..806dae084d 100644
--- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
+++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
@@ -49,7 +49,7 @@
@@ -59,7 +59,7 @@
{{ "logOut" | i18n }}
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
index 9a0423fca3..e56a2d5c38 100644
--- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
+++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { AccountSwitcherService } from "./services/account-switcher.service";
@@ -64,9 +65,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
this.location.back();
}
- async lock(userId?: string) {
+ async lock(userId: string) {
this.loading = true;
- await this.vaultTimeoutService.lock(userId ? userId : null);
+ await this.vaultTimeoutService.lock(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["lock"]);
@@ -96,7 +97,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
.subscribe(() => this.router.navigate(["lock"]));
}
- async logOut() {
+ async logOut(userId: UserId) {
this.loading = true;
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
@@ -105,7 +106,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
});
if (confirmed) {
- this.messagingService.send("logout");
+ this.messagingService.send("logout", { userId });
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
index fe04bee20e..d27410a5d0 100644
--- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
+++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts
@@ -58,6 +58,7 @@ describe("AccountSwitcherService", () => {
const accountInfo: AccountInfo = {
name: "Test User 1",
email: "test1@email.com",
+ emailVerified: true,
};
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
@@ -89,6 +90,7 @@ describe("AccountSwitcherService", () => {
for (let i = 0; i < numberOfAccounts; i++) {
seedAccounts[`${i}` as UserId] = {
email: `test${i}@email.com`,
+ emailVerified: true,
name: "Test User ${i}",
};
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
@@ -113,6 +115,7 @@ describe("AccountSwitcherService", () => {
const user1AccountInfo: AccountInfo = {
name: "Test User 1",
email: "",
+ emailVerified: true,
};
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
index a73ec3e1f6..e5a3b8f8f5 100644
--- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
+++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts
@@ -110,7 +110,7 @@ export class AccountSwitcherService {
}),
);
- // Create a reusable observable that listens to the the switchAccountFinish message and returns the userId from the message
+ // Create a reusable observable that listens to the switchAccountFinish message and returns the userId from the message
this.switchAccountFinished$ = fromChromeEvent<[message: { command: string; userId: string }]>(
chrome.runtime.onMessage,
).pipe(
diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts
index 4d47417df6..86352e2c82 100644
--- a/apps/browser/src/auth/popup/lock.component.ts
+++ b/apps/browser/src/auth/popup/lock.component.ts
@@ -59,7 +59,7 @@ export class LockComponent extends BaseLockComponent {
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
- private authService: AuthService,
+ authService: AuthService,
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
@@ -92,6 +92,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService,
biometricStateService,
accountService,
+ authService,
kdfConfigService,
);
this.successRoute = "/tabs/current";
diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts
index 8cdfa0f027..2eb976529f 100644
--- a/apps/browser/src/autofill/background/web-request.background.ts
+++ b/apps/browser/src/autofill/background/web-request.background.ts
@@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
-import { BrowserApi } from "../../platform/browser/browser-api";
-
export default class WebRequestBackground {
- private pendingAuthRequests: any[] = [];
- private webRequest: any;
+ private pendingAuthRequests: Set = new Set([]);
private isFirefox: boolean;
constructor(
platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private authService: AuthService,
+ private readonly webRequest: typeof chrome.webRequest,
) {
- if (BrowserApi.isManifestVersion(2)) {
- this.webRequest = chrome.webRequest;
- }
this.isFirefox = platformUtilsService.isFirefox();
}
- async init() {
- if (!this.webRequest || !this.webRequest.onAuthRequired) {
- return;
- }
-
+ startListening() {
this.webRequest.onAuthRequired.addListener(
- async (details: any, callback: any) => {
- if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
+ async (details, callback) => {
+ if (!details.url || this.pendingAuthRequests.has(details.requestId)) {
if (callback) {
- callback();
+ callback(null);
}
return;
}
-
- this.pendingAuthRequests.push(details.requestId);
-
+ this.pendingAuthRequests.add(details.requestId);
if (this.isFirefox) {
// eslint-disable-next-line
return new Promise(async (resolve, reject) => {
@@ -51,7 +40,7 @@ export default class WebRequestBackground {
[this.isFirefox ? "blocking" : "asyncBlocking"],
);
- this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
+ this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), {
urls: ["http://*/*"],
});
this.webRequest.onErrorOccurred.addListener(
@@ -91,10 +80,7 @@ export default class WebRequestBackground {
}
}
- private completeAuthRequest(details: any) {
- const i = this.pendingAuthRequests.indexOf(details.requestId);
- if (i > -1) {
- this.pendingAuthRequests.splice(i, 1);
- }
+ private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) {
+ this.pendingAuthRequests.delete(details.requestId);
}
}
diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
index e54f37489b..6ef004f797 100644
--- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
+++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
@@ -11,7 +11,8 @@ import {
GENERATE_PASSWORD_ID,
NOOP_COMMAND_SUFFIX,
} from "@bitwarden/common/autofill/constants";
-import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
+import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -65,7 +66,7 @@ describe("ContextMenuClickedHandler", () => {
let autofill: AutofillAction;
let authService: MockProxy;
let cipherService: MockProxy;
- let stateService: MockProxy;
+ let accountService: FakeAccountService;
let totpService: MockProxy;
let eventCollectionService: MockProxy;
let userVerificationService: MockProxy;
@@ -78,7 +79,7 @@ describe("ContextMenuClickedHandler", () => {
autofill = jest.fn, [tab: chrome.tabs.Tab, cipher: CipherView]>();
authService = mock();
cipherService = mock();
- stateService = mock();
+ accountService = mockAccountServiceWith("userId" as UserId);
totpService = mock();
eventCollectionService = mock();
@@ -88,10 +89,10 @@ describe("ContextMenuClickedHandler", () => {
autofill,
authService,
cipherService,
- stateService,
totpService,
eventCollectionService,
userVerificationService,
+ accountService,
);
});
diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
index 596d6b7235..5ba48a9f27 100644
--- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
+++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
@@ -1,4 +1,7 @@
+import { firstValueFrom, map } from "rxjs";
+
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
@@ -17,7 +20,6 @@ import {
NOOP_COMMAND_SUFFIX,
} from "@bitwarden/common/autofill/constants";
import { EventType } from "@bitwarden/common/enums";
-import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -26,6 +28,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
import {
authServiceFactory,
AuthServiceInitOptions,
@@ -37,7 +40,6 @@ import { autofillSettingsServiceFactory } from "../../autofill/background/servic
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
import { Account } from "../../models/account";
import { CachedServices } from "../../platform/background/service-factories/factory-options";
-import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory";
import { BrowserApi } from "../../platform/browser/browser-api";
import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory";
import {
@@ -71,10 +73,10 @@ export class ContextMenuClickedHandler {
private autofillAction: AutofillAction,
private authService: AuthService,
private cipherService: CipherService,
- private stateService: StateService,
private totpService: TotpService,
private eventCollectionService: EventCollectionService,
private userVerificationService: UserVerificationService,
+ private accountService: AccountService,
) {}
static async mv3Create(cachedServices: CachedServices) {
@@ -128,10 +130,10 @@ export class ContextMenuClickedHandler {
(tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions),
- await stateServiceFactory(cachedServices, serviceOptions),
await totpServiceFactory(cachedServices, serviceOptions),
await eventCollectionServiceFactory(cachedServices, serviceOptions),
await userVerificationServiceFactory(cachedServices, serviceOptions),
+ await accountServiceFactory(cachedServices, serviceOptions),
);
}
@@ -239,9 +241,10 @@ export class ContextMenuClickedHandler {
return;
}
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stateService.setLastActive(new Date().getTime());
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+ await this.accountService.setAccountActivity(activeUserId, new Date());
switch (info.parentMenuItemId) {
case AUTOFILL_ID:
case AUTOFILL_IDENTITY_ID:
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 758c226bc3..adcb4a21ba 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -1,4 +1,4 @@
-import { Subject, firstValueFrom, merge } from "rxjs";
+import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
import {
PinCryptoServiceAbstraction,
@@ -71,6 +71,7 @@ import {
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
+import { ClientType } from "@bitwarden/common/enums";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -112,7 +113,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
-import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service";
+import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import {
ActiveUserStateProvider,
@@ -333,7 +334,7 @@ export default class MainBackground {
billingAccountProfileStateService: BillingAccountProfileStateService;
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
intraprocessMessagingSubject: Subject>;
- userKeyInitService: UserKeyInitService;
+ userAutoUnlockKeyService: UserAutoUnlockKeyService;
scriptInjectorService: BrowserScriptInjectorService;
kdfConfigService: kdfConfigServiceAbstraction;
@@ -490,7 +491,7 @@ export default class MainBackground {
this.accountService,
this.singleUserStateProvider,
);
- this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
+ this.derivedStateProvider = new BackgroundDerivedStateProvider();
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
this.singleUserStateProvider,
@@ -520,6 +521,7 @@ export default class MainBackground {
this.storageService,
this.logService,
new MigrationBuilderService(),
+ ClientType.Browser,
);
this.stateService = new DefaultBrowserStateService(
@@ -900,6 +902,7 @@ export default class MainBackground {
this.autofillSettingsService,
this.vaultTimeoutSettingsService,
this.biometricStateService,
+ this.accountService,
);
// Other fields
@@ -918,7 +921,6 @@ export default class MainBackground {
this.autofillService,
this.platformUtilsService as BrowserPlatformUtilsService,
this.notificationsService,
- this.stateService,
this.autofillSettingsService,
this.systemService,
this.environmentService,
@@ -927,6 +929,7 @@ export default class MainBackground {
this.configService,
this.fido2Background,
messageListener,
+ this.accountService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.accountService,
@@ -1016,10 +1019,10 @@ export default class MainBackground {
},
this.authService,
this.cipherService,
- this.stateService,
this.totpService,
this.eventCollectionService,
this.userVerificationService,
+ this.accountService,
);
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
@@ -1053,20 +1056,17 @@ export default class MainBackground {
this.cipherService,
);
- if (BrowserApi.isManifestVersion(2)) {
+ if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService,
this.authService,
+ chrome.webRequest,
);
}
}
- this.userKeyInitService = new UserKeyInitService(
- this.accountService,
- this.cryptoService,
- this.logService,
- );
+ this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
}
async bootstrap() {
@@ -1077,7 +1077,18 @@ export default class MainBackground {
// This is here instead of in in the InitService b/c we don't plan for
// side effects to run in the Browser InitService.
- this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
+ const accounts = await firstValueFrom(this.accountService.accounts$);
+
+ const setUserKeyInMemoryPromises = [];
+ for (const userId of Object.keys(accounts) as UserId[]) {
+ // For each acct, we must await the process of setting the user key in memory
+ // if the auto user key is set to avoid race conditions of any code trying to access
+ // the user key from mem.
+ setUserKeyInMemoryPromises.push(
+ this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId),
+ );
+ }
+ await Promise.all(setUserKeyInMemoryPromises);
await (this.i18nService as I18nService).init();
(this.eventUploadService as EventUploadService).init(true);
@@ -1096,9 +1107,7 @@ export default class MainBackground {
await this.tabsBackground.init();
this.contextMenusBackground?.init();
await this.idleBackground.init();
- if (BrowserApi.isManifestVersion(2)) {
- await this.webRequestBackground.init();
- }
+ this.webRequestBackground?.startListening();
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
// Set Private Mode windows to the default icon - they do not share state with the background page
@@ -1159,7 +1168,12 @@ export default class MainBackground {
*/
async switchAccount(userId: UserId) {
try {
- await this.stateService.setActiveUser(userId);
+ const currentlyActiveAccount = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((account) => account?.id)),
+ );
+ // can be removed once password generation history is migrated to state providers
+ await this.stateService.clearDecryptedData(currentlyActiveAccount);
+ await this.accountService.switchAccount(userId);
if (userId == null) {
this.loginEmailService.setRememberEmail(false);
@@ -1196,7 +1210,18 @@ export default class MainBackground {
}
async logout(expired: boolean, userId?: UserId) {
- userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ userId ??= (
+ await firstValueFrom(
+ this.accountService.activeAccount$.pipe(
+ timeout({
+ first: 2000,
+ with: () => {
+ throw new Error("No active account found to logout");
+ },
+ }),
+ ),
+ )
+ )?.id;
await this.eventUploadService.uploadEvents(userId as UserId);
@@ -1220,7 +1245,11 @@ export default class MainBackground {
//Needs to be checked before state is cleaned
const needStorageReseed = await this.needsStorageReseed();
- const newActiveUser = await this.stateService.clean({ userId: userId });
+ const newActiveUser = await firstValueFrom(
+ this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
+ );
+ await this.stateService.clean({ userId: userId });
+ await this.accountService.clean(userId);
await this.stateEventRunnerService.handleEvent("logout", userId);
diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts
index 294346fe9f..d8f3cf840f 100644
--- a/apps/browser/src/background/runtime.background.ts
+++ b/apps/browser/src/background/runtime.background.ts
@@ -1,6 +1,7 @@
-import { firstValueFrom, mergeMap } from "rxjs";
+import { firstValueFrom, map, mergeMap } from "rxjs";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -19,7 +20,6 @@ import {
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../platform/browser/browser-api";
-import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background";
@@ -37,7 +37,6 @@ export default class RuntimeBackground {
private autofillService: AutofillService,
private platformUtilsService: BrowserPlatformUtilsService,
private notificationsService: NotificationsService,
- private stateService: BrowserStateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private systemService: SystemService,
private environmentService: BrowserEnvironmentService,
@@ -46,6 +45,7 @@ export default class RuntimeBackground {
private configService: ConfigService,
private fido2Background: Fido2Background,
private messageListener: MessageListener,
+ private accountService: AccountService,
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@@ -76,7 +76,8 @@ export default class RuntimeBackground {
void this.processMessageWithSender(msg, sender).catch((err) =>
this.logService.error(
- `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`,
+ `Error while processing message in RuntimeBackground '${msg?.command}'.`,
+ err,
),
);
return false;
@@ -85,7 +86,11 @@ export default class RuntimeBackground {
this.messageListener.allMessages$
.pipe(
mergeMap(async (message: any) => {
- await this.processMessage(message);
+ try {
+ await this.processMessage(message);
+ } catch (err) {
+ this.logService.error(err);
+ }
}),
)
.subscribe();
@@ -107,9 +112,10 @@ export default class RuntimeBackground {
switch (msg.sender) {
case "autofiller":
case "autofill_cmd": {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stateService.setLastActive(new Date().getTime());
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+ await this.accountService.setAccountActivity(activeUserId, new Date());
const totpCode = await this.autofillService.doAutoFillActiveTab(
[
{
diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json
index c0c88706b8..b7aaba2e0e 100644
--- a/apps/browser/src/manifest.v3.json
+++ b/apps/browser/src/manifest.v3.json
@@ -60,7 +60,9 @@
"clipboardWrite",
"idle",
"scripting",
- "offscreen"
+ "offscreen",
+ "webRequest",
+ "webRequestAuthProvider"
],
"optional_permissions": ["nativeMessaging", "privacy"],
"host_permissions": [""],
diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts
index 4025d01950..3c3900144b 100644
--- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts
+++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts
@@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
import { CachedServices, FactoryOptions, factory } from "./factory-options";
-import {
- StorageServiceProviderInitOptions,
- storageServiceProviderFactory,
-} from "./storage-service-provider.factory";
type DerivedStateProviderFactoryOptions = FactoryOptions;
-export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
- StorageServiceProviderInitOptions;
+export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
export async function derivedStateProviderFactory(
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
@@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
cache,
"derivedStateProvider",
opts,
- async () =>
- new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
+ async () => new BackgroundDerivedStateProvider(),
);
}
diff --git a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts
index a49699a615..090531f7cf 100644
--- a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts
+++ b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts
@@ -1,3 +1,4 @@
+import { ClientType } from "@bitwarden/common/enums";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
@@ -27,6 +28,7 @@ export async function migrationRunnerFactory(
await diskStorageServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
new MigrationBuilderService(),
+ ClientType.Browser,
),
);
}
diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts
index b793777d8b..f536eb8312 100644
--- a/apps/browser/src/platform/browser/browser-api.ts
+++ b/apps/browser/src/platform/browser/browser-api.ts
@@ -238,10 +238,6 @@ export class BrowserApi {
return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage();
}
- static getApplicationVersion(): string {
- return chrome.runtime.getManifest().version;
- }
-
/**
* Gets the extension views that match the given properties. This method is not
* available within background service worker. As a result, it will return an
diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
index 1cbcc7a94c..933cd08c2e 100644
--- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
+++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts
@@ -28,6 +28,7 @@ describe("OffscreenDocument", () => {
});
it("shows a console message if the handler throws an error", async () => {
+ const error = new Error("test error");
browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error"));
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" });
@@ -35,7 +36,8 @@ describe("OffscreenDocument", () => {
expect(browserClipboardServiceCopySpy).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
- "Error resolving extension message response: Error: test error",
+ "Error resolving extension message response",
+ error,
);
});
diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts
index 627036b80b..4994a6e9ba 100644
--- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts
+++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts
@@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface {
Promise.resolve(messageResponse)
.then((response) => sendResponse(response))
.catch((error) =>
- this.consoleLogService.error(`Error resolving extension message response: ${error}`),
+ this.consoleLogService.error("Error resolving extension message response", error),
);
return true;
};
diff --git a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts
index e84cd19a45..c2d33369bd 100644
--- a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts
+++ b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts
@@ -203,7 +203,7 @@ describe("BrowserPopupUtils", () => {
expect(BrowserPopupUtils["buildPopoutUrl"]).not.toHaveBeenCalled();
});
- it("replaces any existing `uilocation=` query params within the passed extension url path to state the the uilocaiton is a popup", async () => {
+ it("replaces any existing `uilocation=` query params within the passed extension url path to state the uilocation is a popup", async () => {
const url = "popup/index.html?uilocation=sidebar#/tabs/vault";
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts
index ebda12c2a4..1373837866 100644
--- a/apps/browser/src/platform/popup/header.component.ts
+++ b/apps/browser/src/platform/popup/header.component.ts
@@ -1,10 +1,8 @@
import { Component, Input } from "@angular/core";
-import { Observable, combineLatest, map, of, switchMap } from "rxjs";
+import { Observable, map, of, switchMap } 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 { UserId } from "@bitwarden/common/types/guid";
import { enableAccountSwitching } from "../flags";
@@ -16,18 +14,15 @@ export class HeaderComponent {
@Input() noTheme = false;
@Input() hideAccountSwitcher = false;
authedAccounts$: Observable;
- constructor(accountService: AccountService, authService: AuthService) {
- this.authedAccounts$ = accountService.accounts$.pipe(
- switchMap((accounts) => {
+ constructor(authService: AuthService) {
+ this.authedAccounts$ = authService.authStatuses$.pipe(
+ map((record) => Object.values(record)),
+ switchMap((statuses) => {
if (!enableAccountSwitching()) {
return of(false);
}
- return combineLatest(
- Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)),
- ).pipe(
- map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)),
- );
+ return of(statuses.some((status) => status !== AuthenticationStatus.LoggedOut));
}),
);
}
diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html
new file mode 100644
index 0000000000..2cbbca79c0
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html
@@ -0,0 +1,9 @@
+
diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.ts b/apps/browser/src/platform/popup/layout/popup-footer.component.ts
new file mode 100644
index 0000000000..826a1d1c60
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-footer.component.ts
@@ -0,0 +1,9 @@
+import { Component } from "@angular/core";
+
+@Component({
+ selector: "popup-footer",
+ templateUrl: "popup-footer.component.html",
+ standalone: true,
+ imports: [],
+})
+export class PopupFooterComponent {}
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html
new file mode 100644
index 0000000000..c0894f8168
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-header.component.html
@@ -0,0 +1,19 @@
+
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts
new file mode 100644
index 0000000000..f2f8eb95af
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts
@@ -0,0 +1,34 @@
+import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
+import { CommonModule, Location } from "@angular/common";
+import { Component, Input } from "@angular/core";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { IconButtonModule, TypographyModule } from "@bitwarden/components";
+
+@Component({
+ selector: "popup-header",
+ templateUrl: "popup-header.component.html",
+ standalone: true,
+ imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule],
+})
+export class PopupHeaderComponent {
+ /** Display the back button, which uses Location.back() to go back one page in history */
+ @Input()
+ get showBackButton() {
+ return this._showBackButton;
+ }
+ set showBackButton(value: BooleanInput) {
+ this._showBackButton = coerceBooleanProperty(value);
+ }
+
+ private _showBackButton = false;
+
+ /** Title string that will be inserted as an h1 */
+ @Input({ required: true }) pageTitle: string;
+
+ constructor(private location: Location) {}
+
+ back() {
+ this.location.back();
+ }
+}
diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx
new file mode 100644
index 0000000000..91f7dab277
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx
@@ -0,0 +1,138 @@
+import { Meta, Story, Canvas } from "@storybook/addon-docs";
+
+import * as stories from "./popup-layout.stories";
+
+
+
+Please note that because these stories use `router-outlet`, there are issues with rendering content
+when Light & Dark mode is selected. The stories are best viewed by selecting one color mode.
+
+# Popup Tab Navigation
+
+The popup tab navigation component composes together the popup page and the bottom tab navigation
+footer. This component is intended to be used a level _above_ each extension tab's page code.
+
+The navigation footer contains the 4 main page links for the browser extension. It uses the Angular
+router to determine which page is currently active, and style the button appropriately. Clicking on
+the buttons will navigate to the correct route. The navigation footer has a max-width built in so
+that the page looks nice when the extension is popped out.
+
+Long button names will be ellipsed.
+
+Usage example:
+
+```html
+
+
+
+```
+
+# Popup Page
+
+The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest
+of the content into the `main` element with scroll. There is also a max-width built in so that the
+page looks nice when the extension is popped out.
+
+**Slots**
+
+- `header`
+ - Use `popup-header` component.
+ - Every page should have a header.
+- `footer`
+ - Use the `popup-footer` component.
+ - Not every page will have a footer.
+- default
+ - Whatever content you want in `main`.
+
+Basic usage example:
+
+```html
+
+
+ This is content
+
+
+```
+
+## Popup header
+
+**Args**
+
+- `pageTitle`: required
+ - Inserts title as an `h1`.
+- `showBackButton`: optional, defaults to `false`
+ - Toggles the back button to appear. The back button uses `Location.back()` to navigate back one
+ page in history.
+
+**Slots**
+
+- `end`
+ - Use to insert one or more interactive elements.
+ - The header handles the spacing between elements passed to the `end` slot.
+
+Usage example:
+
+```html
+
+
+
+
+
+
+```
+
+Common interactive elements to insert into the `end` slot are:
+
+- `app-current-account`: shows current account and switcher
+- `app-pop-out`: shows popout button when the extension is not already popped out
+- "Add" button: this can be accomplished with the Button component and any custom functionality for
+ that particular page
+
+## Popup footer
+
+Popup footer should be used when the page displays action buttons. It functions similarly to the
+Dialog footer in that the calling code is responsible for passing in the different buttons that need
+to be rendered.
+
+Usage example:
+
+```html
+
+ Save
+ Cancel
+
+```
+
+# Page types
+
+There are a few types of pages that are used in the browser extension.
+
+View the story source code to see examples of how to construct these types of pages.
+
+## Extension Tab
+
+Example of wrapping an extension page in the `popup-tab-navigation` component.
+
+
+
+
+
+## Extension Page
+
+Examples of using just the `popup-page` component, without and with a footer.
+
+
+
+
+
+
+
+
+
+## Popped out
+
+When the browser extension is popped out, the "popout" button should not be passed to the header.
+
+
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts
new file mode 100644
index 0000000000..77530d06e5
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts
@@ -0,0 +1,380 @@
+import { CommonModule } from "@angular/common";
+import { Component, importProvidersFrom } from "@angular/core";
+import { RouterModule } from "@angular/router";
+import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
+
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import {
+ AvatarModule,
+ BadgeModule,
+ ButtonModule,
+ I18nMockService,
+ IconButtonModule,
+ ItemModule,
+} from "@bitwarden/components";
+
+import { PopupFooterComponent } from "./popup-footer.component";
+import { PopupHeaderComponent } from "./popup-header.component";
+import { PopupPageComponent } from "./popup-page.component";
+import { PopupTabNavigationComponent } from "./popup-tab-navigation.component";
+
+@Component({
+ selector: "extension-container",
+ template: `
+
+
+
+ `,
+ standalone: true,
+})
+class ExtensionContainerComponent {}
+
+@Component({
+ selector: "vault-placeholder",
+ template: `
+
+
+
+
+ {{ i }} of {{ data.length - 1 }}
+ Bar
+
+
+
+
+ Auto-fill
+
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule],
+})
+class VaultComponent {
+ protected data = Array.from(Array(20).keys());
+}
+
+@Component({
+ selector: "generator-placeholder",
+ template: ` generator stuff here
`,
+ standalone: true,
+})
+class GeneratorComponent {}
+
+@Component({
+ selector: "send-placeholder",
+ template: ` send some stuff
`,
+ standalone: true,
+})
+class SendComponent {}
+
+@Component({
+ selector: "settings-placeholder",
+ template: ` change your settings
`,
+ standalone: true,
+})
+class SettingsComponent {}
+
+@Component({
+ selector: "mock-add-button",
+ template: `
+
+
+ Add
+
+ `,
+ standalone: true,
+ imports: [ButtonModule],
+})
+class MockAddButtonComponent {}
+
+@Component({
+ selector: "mock-popout-button",
+ template: `
+
+ `,
+ standalone: true,
+ imports: [IconButtonModule],
+})
+class MockPopoutButtonComponent {}
+
+@Component({
+ selector: "mock-current-account",
+ template: `
+
+
+
+ `,
+ standalone: true,
+ imports: [AvatarModule],
+})
+class MockCurrentAccountComponent {}
+
+@Component({
+ selector: "mock-vault-page",
+ template: `
+
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ VaultComponent,
+ ],
+})
+class MockVaultPageComponent {}
+
+@Component({
+ selector: "mock-vault-page-popped",
+ template: `
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ VaultComponent,
+ ],
+})
+class MockVaultPagePoppedComponent {}
+
+@Component({
+ selector: "mock-generator-page",
+ template: `
+
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ GeneratorComponent,
+ ],
+})
+class MockGeneratorPageComponent {}
+
+@Component({
+ selector: "mock-send-page",
+ template: `
+
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ SendComponent,
+ ],
+})
+class MockSendPageComponent {}
+
+@Component({
+ selector: "mock-settings-page",
+ template: `
+
+
+
+
+
+
+
+
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ SettingsComponent,
+ ],
+})
+class MockSettingsPageComponent {}
+
+@Component({
+ selector: "mock-vault-subpage",
+ template: `
+
+
+
+
+
+
+
+
+ Save
+ Cancel
+
+
+ `,
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopupFooterComponent,
+ ButtonModule,
+ MockAddButtonComponent,
+ MockPopoutButtonComponent,
+ MockCurrentAccountComponent,
+ VaultComponent,
+ ],
+})
+class MockVaultSubpageComponent {}
+
+export default {
+ title: "Browser/Popup Layout",
+ component: PopupPageComponent,
+ decorators: [
+ moduleMetadata({
+ imports: [
+ PopupTabNavigationComponent,
+ CommonModule,
+ RouterModule,
+ ExtensionContainerComponent,
+ MockVaultSubpageComponent,
+ MockVaultPageComponent,
+ MockSendPageComponent,
+ MockGeneratorPageComponent,
+ MockSettingsPageComponent,
+ MockVaultPagePoppedComponent,
+ ],
+ providers: [
+ {
+ provide: I18nService,
+ useFactory: () => {
+ return new I18nMockService({
+ back: "Back",
+ });
+ },
+ },
+ ],
+ }),
+ applicationConfig({
+ providers: [
+ importProvidersFrom(
+ RouterModule.forRoot(
+ [
+ { path: "", redirectTo: "vault", pathMatch: "full" },
+ { path: "vault", component: MockVaultPageComponent },
+ { path: "generator", component: MockGeneratorPageComponent },
+ { path: "send", component: MockSendPageComponent },
+ { path: "settings", component: MockSettingsPageComponent },
+ // in case you are coming from a story that also uses the router
+ { path: "**", redirectTo: "vault" },
+ ],
+ { useHash: true },
+ ),
+ ),
+ ],
+ }),
+ ],
+} as Meta;
+
+type Story = StoryObj;
+
+export const PopupTabNavigation: Story = {
+ render: (args) => ({
+ props: args,
+ template: /* HTML */ `
+
+
+
+
+
+ `,
+ }),
+};
+
+export const PopupPage: Story = {
+ render: (args) => ({
+ props: args,
+ template: /* HTML */ `
+
+
+
+ `,
+ }),
+};
+
+export const PopupPageWithFooter: Story = {
+ render: (args) => ({
+ props: args,
+ template: /* HTML */ `
+
+
+
+ `,
+ }),
+};
+
+export const PoppedOut: Story = {
+ render: (args) => ({
+ props: args,
+ template: /* HTML */ `
+
+
+
+ `,
+ }),
+};
diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html
new file mode 100644
index 0000000000..ba871d6319
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-page.component.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts
new file mode 100644
index 0000000000..1223a6f418
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts
@@ -0,0 +1,11 @@
+import { Component } from "@angular/core";
+
+@Component({
+ selector: "popup-page",
+ templateUrl: "popup-page.component.html",
+ standalone: true,
+ host: {
+ class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto",
+ },
+})
+export class PopupPageComponent {}
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html
new file mode 100644
index 0000000000..a0ff252c6c
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html
@@ -0,0 +1,32 @@
+
+
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts
new file mode 100644
index 0000000000..3a275454d9
--- /dev/null
+++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts
@@ -0,0 +1,43 @@
+import { CommonModule } from "@angular/common";
+import { Component } from "@angular/core";
+import { RouterModule } from "@angular/router";
+
+import { LinkModule } from "@bitwarden/components";
+
+@Component({
+ selector: "popup-tab-navigation",
+ templateUrl: "popup-tab-navigation.component.html",
+ standalone: true,
+ imports: [CommonModule, LinkModule, RouterModule],
+ host: {
+ class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col",
+ },
+})
+export class PopupTabNavigationComponent {
+ navButtons = [
+ {
+ label: "Vault",
+ page: "/vault",
+ iconKey: "lock",
+ iconKeyActive: "lock-f",
+ },
+ {
+ label: "Generator",
+ page: "/generator",
+ iconKey: "generate",
+ iconKeyActive: "generate-f",
+ },
+ {
+ label: "Send",
+ page: "/send",
+ iconKey: "send",
+ iconKeyActive: "send-f",
+ },
+ {
+ label: "Settings",
+ page: "/settings",
+ iconKey: "cog",
+ iconKeyActive: "cog-f",
+ },
+ ];
+}
diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts
index f06126dcf5..a0a52ff622 100644
--- a/apps/browser/src/platform/services/browser-state.service.spec.ts
+++ b/apps/browser/src/platform/services/browser-state.service.spec.ts
@@ -1,5 +1,4 @@
import { mock, MockProxy } from "jest-mock-extended";
-import { firstValueFrom } from "rxjs";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -50,7 +49,6 @@ describe("Browser State Service", () => {
state.accounts[userId] = new Account({
profile: { userId: userId },
});
- state.activeUserId = userId;
});
afterEach(() => {
@@ -78,18 +76,8 @@ describe("Browser State Service", () => {
);
});
- describe("add Account", () => {
- it("should add account", async () => {
- const newUserId = "newUserId" as UserId;
- const newAcct = new Account({
- profile: { userId: newUserId },
- });
-
- await sut.addAccount(newAcct);
-
- const accts = await firstValueFrom(sut.accounts$);
- expect(accts[newUserId]).toBeDefined();
- });
+ it("exists", () => {
+ expect(sut).toBeDefined();
});
});
});
diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts
index b9cd219076..f717ab96d8 100644
--- a/apps/browser/src/platform/services/default-browser-state.service.ts
+++ b/apps/browser/src/platform/services/default-browser-state.service.ts
@@ -29,8 +29,6 @@ export class DefaultBrowserStateService
initializeAs: "record",
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
- @sessionSync({ initializer: (s: string) => s })
- protected activeAccountSubject: BehaviorSubject;
protected accountDeserializer = Account.fromJSON;
diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts
index 0fa359181d..c29b9c69dc 100644
--- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts
+++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts
@@ -200,26 +200,29 @@ export class LocalBackedSessionStorageService
}
private compareValues(value1: T, value2: T): boolean {
- if (value1 == null && value2 == null) {
+ try {
+ if (value1 == null && value2 == null) {
+ return true;
+ }
+
+ if (value1 && value2 == null) {
+ return false;
+ }
+
+ if (value1 == null && value2) {
+ return false;
+ }
+
+ if (typeof value1 !== "object" || typeof value2 !== "object") {
+ return value1 === value2;
+ }
+
+ return JSON.stringify(value1) === JSON.stringify(value2);
+ } catch (e) {
+ this.logService.error(
+ `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`,
+ );
return true;
}
-
- if (value1 && value2 == null) {
- return false;
- }
-
- if (value1 == null && value2) {
- return false;
- }
-
- if (typeof value1 !== "object" || typeof value2 !== "object") {
- return value1 === value2;
- }
-
- if (JSON.stringify(value1) === JSON.stringify(value2)) {
- return true;
- }
-
- return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString();
}
}
diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts
index e9f7f17d9b..6e3b3aa403 100644
--- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts
+++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts
@@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
}
getApplicationVersion(): Promise {
- return Promise.resolve(BrowserApi.getApplicationVersion());
+ const manifest = chrome.runtime.getManifest();
+ return Promise.resolve(manifest.version_name ?? manifest.version);
}
- async getApplicationVersionNumber(): Promise {
- return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
+ getApplicationVersionNumber(): Promise {
+ const manifest = chrome.runtime.getManifest();
+ return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim());
}
supportsWebAuthn(win: Window): boolean {
diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts
index f3d217789e..cbc5a34b37 100644
--- a/apps/browser/src/platform/state/background-derived-state.provider.ts
+++ b/apps/browser/src/platform/state/background-derived-state.provider.ts
@@ -1,9 +1,5 @@
import { Observable } from "rxjs";
-import {
- AbstractStorageService,
- ObservableStorageService,
-} from "@bitwarden/common/platform/abstractions/storage.service";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
parentState$: Observable,
deriveDefinition: DeriveDefinition,
dependencies: TDeps,
- storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState {
- const [cacheKey, storageService] = storageLocation;
return new BackgroundDerivedState(
parentState$,
deriveDefinition,
- storageService,
- cacheKey,
+ deriveDefinition.buildCacheKey(),
dependencies,
);
}
diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts
index c62795acdc..61768cb970 100644
--- a/apps/browser/src/platform/state/background-derived-state.ts
+++ b/apps/browser/src/platform/state/background-derived-state.ts
@@ -1,10 +1,7 @@
-import { Observable, Subscription } from "rxjs";
+import { Observable, Subscription, concatMap } from "rxjs";
import { Jsonify } from "type-fest";
-import {
- AbstractStorageService,
- ObservableStorageService,
-} from "@bitwarden/common/platform/abstractions/storage.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
@@ -22,11 +19,10 @@ export class BackgroundDerivedState<
constructor(
parentState$: Observable,
deriveDefinition: DeriveDefinition,
- memoryStorage: AbstractStorageService & ObservableStorageService,
portName: string,
dependencies: TDeps,
) {
- super(parentState$, deriveDefinition, memoryStorage, dependencies);
+ super(parentState$, deriveDefinition, dependencies);
// listen for foreground derived states to connect
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
@@ -42,7 +38,20 @@ export class BackgroundDerivedState<
});
port.onMessage.addListener(listenerCallback);
- const stateSubscription = this.state$.subscribe();
+ const stateSubscription = this.state$
+ .pipe(
+ concatMap(async (state) => {
+ await this.sendMessage(
+ {
+ action: "nextState",
+ data: JSON.stringify(state),
+ id: Utils.newGuid(),
+ },
+ port,
+ );
+ }),
+ )
+ .subscribe();
this.portSubscriptions.set(port, stateSubscription);
});
diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts
index a5df01bc98..823c071a4c 100644
--- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts
+++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts
@@ -4,14 +4,13 @@
*/
import { NgZone } from "@angular/core";
-import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
-import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils";
import { mock } from "jest-mock-extended";
import { Subject, firstValueFrom } from "rxjs";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
+import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec";
import { mockPorts } from "../../../spec/mock-port.spec-util";
@@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)),
+ cleanupDelayMs: 1000,
});
// Mock out the runInsideAngular operator so we don't have to deal with zone.js
@@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => {
let foreground: ForegroundDerivedState;
let background: BackgroundDerivedState>;
let parentState$: Subject;
- let memoryStorage: FakeStorageService;
const initialParent = "2020-01-01";
const ngZone = mock();
const portName = "testPort";
@@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
beforeEach(() => {
mockPorts();
parentState$ = new Subject();
- memoryStorage = new FakeStorageService();
- background = new BackgroundDerivedState(
- parentState$,
- deriveDefinition,
- memoryStorage,
- portName,
- {},
- );
- foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
+ background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
+ foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
});
afterEach(() => {
@@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
});
it("should initialize a late-connected foreground", async () => {
- const newForeground = new ForegroundDerivedState(
- deriveDefinition,
- memoryStorage,
- portName,
- ngZone,
- );
- const backgroundEmissions = trackEmissions(background.state$);
+ const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
+ const backgroundTracker = new ObservableTracker(background.state$);
parentState$.next(initialParent);
- await awaitAsync();
+ const foregroundTracker = new ObservableTracker(newForeground.state$);
- const foregroundEmissions = trackEmissions(newForeground.state$);
- await awaitAsync(10);
-
- expect(backgroundEmissions).toEqual([new Date(initialParent)]);
- expect(foregroundEmissions).toEqual([new Date(initialParent)]);
+ expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
+ expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
});
describe("forceValue", () => {
diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts
index d9262e3b6e..8b8d82b914 100644
--- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts
+++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts
@@ -1,11 +1,6 @@
import { NgZone } from "@angular/core";
import { Observable } from "rxjs";
-import {
- AbstractStorageService,
- ObservableStorageService,
-} from "@bitwarden/common/platform/abstractions/storage.service";
-import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@@ -14,19 +9,18 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state";
import { ForegroundDerivedState } from "./foreground-derived-state";
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
- constructor(
- storageServiceProvider: StorageServiceProvider,
- private ngZone: NgZone,
- ) {
- super(storageServiceProvider);
+ constructor(private ngZone: NgZone) {
+ super();
}
override buildDerivedState(
_parentState$: Observable,
deriveDefinition: DeriveDefinition,
_dependencies: TDeps,
- storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState {
- const [cacheKey, storageService] = storageLocation;
- return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone);
+ return new ForegroundDerivedState(
+ deriveDefinition,
+ deriveDefinition.buildCacheKey(),
+ this.ngZone,
+ );
}
}
diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts
index 2c29f39bc1..ee224540c1 100644
--- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts
+++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts
@@ -1,11 +1,5 @@
-/**
- * need to update test environment so structuredClone works appropriately
- * @jest-environment ../../libs/shared/test.environment.ts
- */
-
import { NgZone } from "@angular/core";
-import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
-import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
+import { awaitAsync } from "@bitwarden/common/../spec";
import { mock } from "jest-mock-extended";
import { DeriveDefinition } from "@bitwarden/common/platform/state";
@@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
describe("ForegroundDerivedState", () => {
let sut: ForegroundDerivedState;
- let memoryStorage: FakeStorageService;
const portName = "testPort";
const ngZone = mock();
beforeEach(() => {
- memoryStorage = new FakeStorageService();
- memoryStorage.internalUpdateValuesRequireDeserialization(true);
mockPorts();
- sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
+ sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
});
afterEach(() => {
@@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
expect(disconnectSpy).toHaveBeenCalled();
expect(sut["port"]).toBeNull();
});
-
- it("should emit when the memory storage updates", async () => {
- const dateString = "2020-01-01";
- const emissions = trackEmissions(sut.state$);
-
- await memoryStorage.save(deriveDefinition.storageKey, {
- derived: true,
- value: new Date(dateString),
- });
-
- await awaitAsync();
-
- expect(emissions).toEqual([new Date(dateString)]);
- });
});
diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts
index b9dda763df..6abe363876 100644
--- a/apps/browser/src/platform/state/foreground-derived-state.ts
+++ b/apps/browser/src/platform/state/foreground-derived-state.ts
@@ -6,19 +6,14 @@ import {
filter,
firstValueFrom,
map,
- merge,
of,
share,
switchMap,
tap,
timer,
} from "rxjs";
-import { Jsonify, JsonObject } from "type-fest";
+import { Jsonify } from "type-fest";
-import {
- AbstractStorageService,
- ObservableStorageService,
-} from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
import { DerivedStateDependencies } from "@bitwarden/common/types/state";
@@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
import { runInsideAngular } from "../browser/run-inside-angular.operator";
export class ForegroundDerivedState implements DerivedState {
- private storageKey: string;
private port: chrome.runtime.Port;
private backgroundResponses$: Observable;
state$: Observable;
constructor(
private deriveDefinition: DeriveDefinition,
- private memoryStorage: AbstractStorageService & ObservableStorageService,
private portName: string,
private ngZone: NgZone,
) {
- this.storageKey = deriveDefinition.storageKey;
-
- const initialStorageGet$ = defer(() => {
- return this.getStoredValue();
- }).pipe(
- filter((s) => s.derived),
- map((s) => s.value),
- );
-
- const latestStorage$ = this.memoryStorage.updates$.pipe(
- filter((s) => s.key === this.storageKey),
- switchMap(async (storageUpdate) => {
- if (storageUpdate.updateType === "remove") {
- return null;
- }
-
- return await this.getStoredValue();
- }),
- filter((s) => s.derived),
- map((s) => s.value),
- );
+ const latestValueFromPort$ = (port: chrome.runtime.Port) => {
+ return fromChromeEvent(port.onMessage).pipe(
+ map(([message]) => message as DerivedStateMessage),
+ filter((message) => message.originator === "background" && message.action === "nextState"),
+ map((message) => {
+ const json = JSON.parse(message.data) as Jsonify;
+ return this.deriveDefinition.deserialize(json);
+ }),
+ );
+ };
this.state$ = defer(() => of(this.initializePort())).pipe(
- switchMap(() => merge(initialStorageGet$, latestStorage$)),
+ switchMap(() => latestValueFromPort$(this.port)),
share({
connector: () => new ReplaySubject(1),
resetOnRefCountZero: () =>
@@ -130,28 +112,4 @@ export class ForegroundDerivedState implements DerivedState {
this.port = null;
this.backgroundResponses$ = null;
}
-
- protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> {
- if (this.memoryStorage.valuesRequireDeserialization) {
- const storedJson = await this.memoryStorage.get<
- Jsonify<{ derived: true; value: JsonObject }>
- >(this.storageKey);
-
- if (!storedJson?.derived) {
- return { derived: false, value: null };
- }
-
- const value = this.deriveDefinition.deserialize(storedJson.value as any);
-
- return { derived: true, value };
- } else {
- const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey);
-
- if (!stored?.derived) {
- return { derived: false, value: null };
- }
-
- return { derived: true, value: stored.value };
- }
- }
}
diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts
index 7acaf1ba93..25fac44450 100644
--- a/apps/browser/src/popup/app.component.ts
+++ b/apps/browser/src/popup/app.component.ts
@@ -1,12 +1,14 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
-import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs";
+import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MessageListener } from "@bitwarden/common/platform/messaging";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
@@ -27,8 +29,9 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
`,
})
export class AppComponent implements OnInit, OnDestroy {
- private lastActivity: number = null;
- private activeUserId: string;
+ private lastActivity: Date;
+ private activeUserId: UserId;
+ private recordActivitySubject = new Subject();
private destroy$ = new Subject();
@@ -46,6 +49,7 @@ export class AppComponent implements OnInit, OnDestroy {
private dialogService: DialogService,
private messageListener: MessageListener,
private toastService: ToastService,
+ private accountService: AccountService,
) {}
async ngOnInit() {
@@ -53,14 +57,13 @@ export class AppComponent implements OnInit, OnDestroy {
// Clear them aggressively to make sure this doesn't occur
await this.clearComponentStates();
- this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
- this.activeUserId = userId;
+ this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
+ this.activeUserId = account?.id;
});
this.authService.activeAccountStatus$
.pipe(
- map((status) => status === AuthenticationStatus.Unlocked),
- filter((unlocked) => unlocked),
+ filter((status) => status === AuthenticationStatus.Unlocked),
concatMap(async () => {
await this.recordActivity();
}),
@@ -200,13 +203,13 @@ export class AppComponent implements OnInit, OnDestroy {
return;
}
- const now = new Date().getTime();
- if (this.lastActivity != null && now - this.lastActivity < 250) {
+ const now = new Date();
+ if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
- await this.stateService.setLastActive(now, { userId: this.activeUserId });
+ await this.accountService.setAccountActivity(this.activeUserId, now);
}
private showToast(msg: any) {
diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts
index 5718542b01..2fb582d693 100644
--- a/apps/browser/src/popup/app.module.ts
+++ b/apps/browser/src/popup/app.module.ts
@@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { HeaderComponent } from "../platform/popup/header.component";
+import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
+import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
+import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
@@ -108,6 +112,10 @@ import "../platform/popup/locales";
AccountComponent,
ButtonModule,
ExportScopeCalloutComponent,
+ PopupPageComponent,
+ PopupTabNavigationComponent,
+ PopupFooterComponent,
+ PopupHeaderComponent,
],
declarations: [
ActionButtonsComponent,
diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss
index 73da455941..80c7536087 100644
--- a/apps/browser/src/popup/scss/base.scss
+++ b/apps/browser/src/popup/scss/base.scss
@@ -68,7 +68,7 @@ img {
border: none;
}
-a {
+a:not(popup-page a, popup-tab-navigation a) {
text-decoration: none;
@include themify($themes) {
@@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
}
}
-header:not(bit-callout header, bit-dialog header) {
+header:not(bit-callout header, bit-dialog header, popup-page header) {
height: 44px;
display: flex;
@@ -448,7 +448,7 @@ app-root {
}
}
-main {
+main:not(popup-page main) {
position: absolute;
top: 44px;
bottom: 0;
diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts
index 052e341004..bec278aeeb 100644
--- a/apps/browser/src/popup/services/services.module.ts
+++ b/apps/browser/src/popup/services/services.module.ts
@@ -13,6 +13,7 @@ import {
SYSTEM_THEME_OBSERVABLE,
SafeInjectionToken,
INTRAPROCESS_MESSAGING_SUBJECT,
+ CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
@@ -45,6 +46,7 @@ import {
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
+import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -473,7 +475,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: DerivedStateProvider,
useClass: ForegroundDerivedStateProvider,
- deps: [StorageServiceProvider, NgZone],
+ deps: [NgZone],
}),
safeProvider({
provide: AutofillSettingsServiceAbstraction,
@@ -558,6 +560,10 @@ const safeProviders: SafeProvider[] = [
OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
],
}),
+ safeProvider({
+ provide: CLIENT_TYPE,
+ useValue: ClientType.Browser,
+ }),
];
@NgModule({
diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html
index a4ad0ba801..e68a664ba7 100644
--- a/apps/browser/src/popup/settings/about.component.html
+++ b/apps/browser/src/popup/settings/about.component.html
@@ -5,7 +5,7 @@
Bitwarden
© Bitwarden Inc. 2015-{{ year }}
-
{{ "version" | i18n }}: {{ version }}
+
{{ "version" | i18n }}: {{ version$ | async }}
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts
index 61b5749b51..d7f98c1e7f 100644
--- a/apps/browser/src/popup/settings/about.component.ts
+++ b/apps/browser/src/popup/settings/about.component.ts
@@ -1,14 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
-import { combineLatest, map } from "rxjs";
+import { Observable, combineLatest, defer, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ButtonModule, DialogModule } from "@bitwarden/components";
-import { BrowserApi } from "../../platform/browser/browser-api";
-
@Component({
templateUrl: "about.component.html",
standalone: true,
@@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
})
export class AboutComponent {
protected year = new Date().getFullYear();
- protected version = BrowserApi.getApplicationVersion();
+ protected version$: Observable;
protected data$ = combineLatest([
this.configService.serverConfig$,
@@ -26,5 +25,8 @@ export class AboutComponent {
constructor(
private configService: ConfigService,
private environmentService: EnvironmentService,
- ) {}
+ private platformUtilsService: PlatformUtilsService,
+ ) {
+ this.version$ = defer(() => this.platformUtilsService.getApplicationVersion());
+ }
}
diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts
index fa6c64fcc5..c7e5b7dc95 100644
--- a/apps/browser/src/popup/settings/settings.component.ts
+++ b/apps/browser/src/popup/settings/settings.component.ts
@@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
@@ -86,6 +87,7 @@ export class SettingsComponent implements OnInit {
private destroy$ = new Subject();
constructor(
+ private accountService: AccountService,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
@@ -434,8 +436,9 @@ export class SettingsComponent implements OnInit {
type: "info",
});
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
- this.messagingService.send("logout");
+ this.messagingService.send("logout", { userId: userId });
}
}
diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts
index 07c6408e8d..fed5541f52 100644
--- a/apps/browser/src/tools/background/fileless-importer.background.ts
+++ b/apps/browser/src/tools/background/fileless-importer.background.ts
@@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
return;
}
- const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
+ const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.BrowserFilelessImport,
);
const userAuthStatus = await this.authService.getAuthStatus();
diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts
index baf985b6e9..c20bf7cb8d 100644
--- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts
+++ b/apps/browser/src/tools/popup/send/send-add-edit.component.ts
@@ -6,6 +6,7 @@ import { first } from "rxjs/operators";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -51,6 +52,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
formBuilder: FormBuilder,
private filePopoutUtilsService: FilePopoutUtilsService,
billingAccountProfileStateService: BillingAccountProfileStateService,
+ accountService: AccountService,
) {
super(
i18nService,
@@ -66,6 +68,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
dialogService,
formBuilder,
billingAccountProfileStateService,
+ accountService,
);
}
diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json
index 505f1533ae..e1bf2b7211 100644
--- a/apps/browser/tsconfig.json
+++ b/apps/browser/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"moduleResolution": "node",
"noImplicitAny": true,
+ "allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "ES2020",
diff --git a/apps/cli/package.json b/apps/cli/package.json
index b06caacfd4..d6c449de48 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -71,7 +71,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
- "tldts": "6.1.16",
+ "tldts": "6.1.18",
"zxcvbn": "4.4.2"
}
}
diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts
index 45c394e912..665701639e 100644
--- a/apps/cli/src/bw.ts
+++ b/apps/cli/src/bw.ts
@@ -3,6 +3,7 @@ import * as path from "path";
import { program } from "commander";
import * as jsdom from "jsdom";
+import { firstValueFrom } from "rxjs";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -79,7 +80,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
-import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service";
+import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import {
ActiveUserStateProvider,
DerivedStateProvider,
@@ -236,7 +237,7 @@ export class Main {
biometricStateService: BiometricStateService;
billingAccountProfileStateService: BillingAccountProfileStateService;
providerApiService: ProviderApiServiceAbstraction;
- userKeyInitService: UserKeyInitService;
+ userAutoUnlockKeyService: UserAutoUnlockKeyService;
kdfConfigService: KdfConfigServiceAbstraction;
constructor() {
@@ -314,7 +315,7 @@ export class Main {
this.singleUserStateProvider,
);
- this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
+ this.derivedStateProvider = new DefaultDerivedStateProvider();
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
@@ -344,6 +345,7 @@ export class Main {
this.storageService,
this.logService,
new MigrationBuilderService(),
+ ClientType.Cli,
);
this.stateService = new StateService(
@@ -708,11 +710,7 @@ export class Main {
this.providerApiService = new ProviderApiService(this.apiService);
- this.userKeyInitService = new UserKeyInitService(
- this.accountService,
- this.cryptoService,
- this.logService,
- );
+ this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
}
async run() {
@@ -733,7 +731,7 @@ export class Main {
this.authService.logOut(() => {
/* Do nothing */
});
- const userId = await this.stateService.getUserId();
+ const userId = (await this.stateService.getUserId()) as UserId;
await Promise.all([
this.eventUploadService.uploadEvents(userId as UserId),
this.syncService.setLastSync(new Date(0)),
@@ -744,9 +742,10 @@ export class Main {
this.passwordGenerationService.clear(),
]);
- await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
+ await this.stateEventRunnerService.handleEvent("logout", userId);
await this.stateService.clean();
+ await this.accountService.clean(userId);
process.env.BW_SESSION = null;
}
@@ -756,7 +755,11 @@ export class Main {
this.containerService.attachToGlobal(global);
await this.i18nService.init();
this.twoFactorService.init();
- this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
+
+ const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
+ if (activeAccount) {
+ await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id);
+ }
}
}
diff --git a/apps/cli/src/platform/services/console-log.service.spec.ts b/apps/cli/src/platform/services/console-log.service.spec.ts
index 10a0ad8cca..03598b16e6 100644
--- a/apps/cli/src/platform/services/console-log.service.spec.ts
+++ b/apps/cli/src/platform/services/console-log.service.spec.ts
@@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec";
import { ConsoleLogService } from "./console-log.service";
-let caughtMessage: any = {};
-
describe("CLI Console log service", () => {
+ const error = new Error("this is an error");
+ const obj = { a: 1, b: 2 };
let logService: ConsoleLogService;
+ let consoleSpy: {
+ log: jest.Mock;
+ warn: jest.Mock;
+ error: jest.Mock;
+ };
+
beforeEach(() => {
- caughtMessage = {};
- interceptConsole(caughtMessage);
+ consoleSpy = interceptConsole();
logService = new ConsoleLogService(true);
});
@@ -19,24 +24,21 @@ describe("CLI Console log service", () => {
it("should redirect all console to error if BW_RESPONSE env is true", () => {
process.env.BW_RESPONSE = "true";
- logService.debug("this is a debug message");
- expect(caughtMessage).toMatchObject({
- error: { 0: "this is a debug message" },
- });
+ logService.debug("this is a debug message", error, obj);
+ expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj);
});
it("should not redirect console to error if BW_RESPONSE != true", () => {
process.env.BW_RESPONSE = "false";
- logService.debug("debug");
- logService.info("info");
- logService.warning("warning");
- logService.error("error");
+ logService.debug("debug", error, obj);
+ logService.info("info", error, obj);
+ logService.warning("warning", error, obj);
+ logService.error("error", error, obj);
- expect(caughtMessage).toMatchObject({
- log: { 0: "info" },
- warn: { 0: "warning" },
- error: { 0: "error" },
- });
+ expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj);
+ expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj);
+ expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj);
+ expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj);
});
});
diff --git a/apps/cli/src/platform/services/console-log.service.ts b/apps/cli/src/platform/services/console-log.service.ts
index a35dae71fc..5bdc0b4015 100644
--- a/apps/cli/src/platform/services/console-log.service.ts
+++ b/apps/cli/src/platform/services/console-log.service.ts
@@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService {
super(isDev, filter);
}
- write(level: LogLevelType, message: string) {
+ write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
if (process.env.BW_RESPONSE === "true") {
// eslint-disable-next-line
- console.error(message);
+ console.error(message, ...optionalParams);
return;
}
- super.write(level, message);
+ super.write(level, message, ...optionalParams);
}
}
diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts
index 4fc19c8433..bb8deb2339 100644
--- a/apps/desktop/src/app/app-routing.module.ts
+++ b/apps/desktop/src/app/app-routing.module.ts
@@ -9,7 +9,7 @@ import {
} from "@bitwarden/angular/auth/guards";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
-import { LoginGuard } from "../auth/guards/login.guard";
+import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
@@ -40,7 +40,7 @@ const routes: Routes = [
{
path: "login",
component: LoginComponent,
- canActivate: [LoginGuard],
+ canActivate: [maxAccountsGuardFn()],
},
{
path: "login-with-device",
diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts
index ad99a3a447..4e540efdc6 100644
--- a/apps/desktop/src/app/app.component.ts
+++ b/apps/desktop/src/app/app.component.ts
@@ -8,7 +8,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { Router } from "@angular/router";
-import { firstValueFrom, Subject, takeUntil } from "rxjs";
+import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -18,9 +18,9 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
-import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -107,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy {
loading = false;
- private lastActivity: number = null;
+ private lastActivity: Date = null;
private modal: ModalRef = null;
private idleTimer: number = null;
private isIdle = false;
- private activeUserId: string = null;
+ private activeUserId: UserId = null;
private destroy$ = new Subject();
@@ -150,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy {
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private providerService: ProviderService,
- private organizationService: InternalOrganizationServiceAbstraction,
+ private accountService: AccountService,
) {}
ngOnInit() {
- this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
- this.activeUserId = userId;
+ this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
+ this.activeUserId = account?.id;
});
this.ngZone.runOutsideAngular(() => {
@@ -400,7 +400,8 @@ export class AppComponent implements OnInit, OnDestroy {
break;
case "switchAccount": {
if (message.userId != null) {
- await this.stateService.setActiveUser(message.userId);
+ await this.stateService.clearDecryptedData(message.userId);
+ await this.accountService.switchAccount(message.userId);
}
const locked =
(await this.authService.getAuthStatus(message.userId)) ===
@@ -522,7 +523,7 @@ export class AppComponent implements OnInit, OnDestroy {
private async updateAppMenu() {
let updateRequest: MenuUpdateRequest;
- const stateAccounts = await firstValueFrom(this.stateService.accounts$);
+ const stateAccounts = await firstValueFrom(this.accountService.accounts$);
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
updateRequest = {
accounts: null,
@@ -531,32 +532,32 @@ export class AppComponent implements OnInit, OnDestroy {
} else {
const accounts: { [userId: string]: MenuAccount } = {};
for (const i in stateAccounts) {
+ const userId = i as UserId;
if (
i != null &&
- stateAccounts[i]?.profile?.userId != null &&
- !this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up
+ userId != null &&
+ !this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
) {
- const userId = stateAccounts[i].profile.userId;
const availableTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
);
+ const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
accounts[userId] = {
- isAuthenticated: await this.stateService.getIsAuthenticated({
- userId: userId,
- }),
- isLocked:
- (await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
+ isAuthenticated: authStatus >= AuthenticationStatus.Locked,
+ isLocked: authStatus === AuthenticationStatus.Locked,
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
- email: stateAccounts[i].profile.email,
- userId: stateAccounts[i].profile.userId,
+ email: stateAccounts[userId].email,
+ userId: userId,
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
};
}
}
updateRequest = {
accounts: accounts,
- activeUserId: await this.stateService.getUserId(),
+ activeUserId: await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ ),
};
}
@@ -564,7 +565,9 @@ export class AppComponent implements OnInit, OnDestroy {
}
private async logOut(expired: boolean, userId?: string) {
- const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId });
+ const userBeingLoggedOut =
+ (userId as UserId) ??
+ (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
// doesn't attempt to update a user that is being logged out as we will manually
@@ -572,9 +575,10 @@ export class AppComponent implements OnInit, OnDestroy {
this.startAccountCleanUp(userBeingLoggedOut);
let preLogoutActiveUserId;
+ const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
try {
// Provide the userId of the user to upload events for
- await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId);
+ await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut);
@@ -582,22 +586,23 @@ export class AppComponent implements OnInit, OnDestroy {
await this.collectionService.clear(userBeingLoggedOut);
await this.passwordGenerationService.clear(userBeingLoggedOut);
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
- await this.biometricStateService.logout(userBeingLoggedOut as UserId);
+ await this.biometricStateService.logout(userBeingLoggedOut);
- await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
+ await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });
+ await this.accountService.clean(userBeingLoggedOut);
} finally {
this.finishAccountCleanUp(userBeingLoggedOut);
}
- if (this.activeUserId == null) {
+ if (nextUpAccount == null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["login"]);
- } else if (preLogoutActiveUserId !== this.activeUserId) {
- this.messagingService.send("switchAccount");
+ } else if (preLogoutActiveUserId !== nextUpAccount.id) {
+ this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
}
await this.updateAppMenu();
@@ -622,13 +627,13 @@ export class AppComponent implements OnInit, OnDestroy {
return;
}
- const now = new Date().getTime();
- if (this.lastActivity != null && now - this.lastActivity < 250) {
+ const now = new Date();
+ if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
- await this.stateService.setLastActive(now, { userId: this.activeUserId });
+ await this.accountService.setAccountActivity(this.activeUserId, now);
// Idle states
if (this.isIdle) {
diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html
index eedafbcfe0..b5741a1a1b 100644
--- a/apps/desktop/src/app/layout/account-switcher.component.html
+++ b/apps/desktop/src/app/layout/account-switcher.component.html
@@ -1,110 +1,112 @@
-
-
-
-
-
{{ activeAccount.email }}
-
{{ activeAccount.server }}
-
({{ "switchAccount" | i18n }})
-
-
-
- {{ "switchAccount" | i18n }}
-
-
-
-
-
-
+
- 0">
-
-
-
- {{ "switchAccount" | i18n }}:
- {{ account.value.email }}
-
- / {{ account.value.server }}
-
- ( {{
- (account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
- | i18n
- }})
-
-
-
-
-
- 0">
-
-
- {{ "addAccount" | i18n }}
-
-
-
- {{ "accountSwitcherLimitReached" | i18n }}
-
+
+
+
+
{{ view.activeAccount.email }}
+
{{ view.activeAccount.server }}
+
({{ "switchAccount" | i18n }})
+
-
-
+
+ {{ "switchAccount" | i18n }}
+
+
+
+
+
+
+
0">
+
+
+
+ {{ "switchAccount" | i18n }}:
+ {{ account.value.email }}
+
+ / {{ account.value.server }}
+
+ ( {{
+ (account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
+ | i18n
+ }})
+
+
+
+
+
+ 0">
+
+
+ {{ "addAccount" | i18n }}
+
+
+
+ {{ "accountSwitcherLimitReached" | i18n }}
+
+
+
+
+
diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts
index 4e39ab0029..c8a26065c1 100644
--- a/apps/desktop/src/app/layout/account-switcher.component.ts
+++ b/apps/desktop/src/app/layout/account-switcher.component.ts
@@ -1,19 +1,17 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay";
-import { Component, OnDestroy, OnInit } from "@angular/core";
+import { Component } from "@angular/core";
import { Router } from "@angular/router";
-import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
+import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
+import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
-import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
-import { Account } from "@bitwarden/common/platform/models/domain/account";
import { UserId } from "@bitwarden/common/types/guid";
type ActiveAccount = {
@@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & {
]),
],
})
-export class AccountSwitcherComponent implements OnInit, OnDestroy {
- activeAccount?: ActiveAccount;
- inactiveAccounts: { [userId: string]: InactiveAccount } = {};
-
+export class AccountSwitcherComponent {
+ activeAccount$: Observable
;
+ inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
authStatus = AuthenticationStatus;
+ view$: Observable<{
+ activeAccount: ActiveAccount | null;
+ inactiveAccounts: { [userId: string]: InactiveAccount };
+ numberOfAccounts: number;
+ showSwitcher: boolean;
+ }>;
+
isOpen = false;
overlayPosition: ConnectedPosition[] = [
{
@@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
},
];
- private destroy$ = new Subject();
+ showSwitcher$: Observable;
- get showSwitcher() {
- const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email);
- const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0;
- return userIsInAVault || userIsAddingAnAdditionalAccount;
- }
-
- get numberOfAccounts() {
- if (this.inactiveAccounts == null) {
- this.isOpen = false;
- return 0;
- }
- return Object.keys(this.inactiveAccounts).length;
- }
+ numberOfAccounts$: Observable;
constructor(
private stateService: StateService,
@@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private avatarService: AvatarService,
private messagingService: MessagingService,
private router: Router,
- private tokenService: TokenService,
private environmentService: EnvironmentService,
private loginEmailService: LoginEmailServiceAbstraction,
- ) {}
+ private accountService: AccountService,
+ ) {
+ this.activeAccount$ = this.accountService.activeAccount$.pipe(
+ switchMap(async (active) => {
+ if (active == null) {
+ return null;
+ }
- async ngOnInit(): Promise {
- this.stateService.accounts$
- .pipe(
- concatMap(async (accounts: { [userId: string]: Account }) => {
- this.inactiveAccounts = await this.createInactiveAccounts(accounts);
+ return {
+ id: active.id,
+ name: active.name,
+ email: active.email,
+ avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
+ server: (await this.environmentService.getEnvironment())?.getHostname(),
+ };
+ }),
+ );
+ this.inactiveAccounts$ = combineLatest([
+ this.activeAccount$,
+ this.accountService.accounts$,
+ this.authService.authStatuses$,
+ ]).pipe(
+ switchMap(async ([activeAccount, accounts, accountStatuses]) => {
+ // Filter out logged out accounts and active account
+ accounts = Object.fromEntries(
+ Object.entries(accounts).filter(
+ ([id]: [UserId, AccountInfo]) =>
+ accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id,
+ ),
+ );
+ return this.createInactiveAccounts(accounts);
+ }),
+ );
+ this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe(
+ map(([activeAccount, inactiveAccounts]) => {
+ const hasActiveUser = activeAccount != null;
+ const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0;
+ return hasActiveUser || userIsAddingAnAdditionalAccount;
+ }),
+ );
+ this.numberOfAccounts$ = this.inactiveAccounts$.pipe(
+ map((accounts) => Object.keys(accounts).length),
+ );
- try {
- this.activeAccount = {
- id: await this.tokenService.getUserId(),
- name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
- email: await this.tokenService.getEmail(),
- avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
- server: (await this.environmentService.getEnvironment())?.getHostname(),
- };
- } catch {
- this.activeAccount = undefined;
- }
- }),
- takeUntil(this.destroy$),
- )
- .subscribe();
- }
-
- ngOnDestroy(): void {
- this.destroy$.next();
- this.destroy$.complete();
+ this.view$ = combineLatest([
+ this.activeAccount$,
+ this.inactiveAccounts$,
+ this.numberOfAccounts$,
+ this.showSwitcher$,
+ ]).pipe(
+ map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({
+ activeAccount,
+ inactiveAccounts,
+ numberOfAccounts,
+ showSwitcher,
+ })),
+ );
}
toggle() {
@@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
await this.loginEmailService.saveEmailSettings();
await this.router.navigate(["/login"]);
- await this.stateService.setActiveUser(null);
+ const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
+ await this.stateService.clearDecryptedData(activeAccount?.id as UserId);
+ await this.accountService.switchAccount(null);
}
private async createInactiveAccounts(baseAccounts: {
- [userId: string]: Account;
+ [userId: string]: AccountInfo;
}): Promise<{ [userId: string]: InactiveAccount }> {
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
@@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
inactiveAccounts[userId] = {
id: userId,
- name: baseAccounts[userId].profile.name,
- email: baseAccounts[userId].profile.email,
+ name: baseAccounts[userId].name,
+ email: baseAccounts[userId].email,
authenticationStatus: await this.authService.getAuthStatus(userId),
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts
index 9a7226218a..06c67d8af2 100644
--- a/apps/desktop/src/app/layout/search/search.component.ts
+++ b/apps/desktop/src/app/layout/search/search.component.ts
@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormControl } from "@angular/forms";
import { Subscription } from "rxjs";
-import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SearchBarService, SearchBarState } from "./search-bar.service";
@@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy {
constructor(
private searchBarService: SearchBarService,
- private stateService: StateService,
+ private accountService: AccountService,
) {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.searchBarService.state$.subscribe((state) => {
@@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy {
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
- this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => {
+ this.activeAccountSubscription = this.accountService.activeAccount$.subscribe((_) => {
this.searchBarService.setSearchText("");
this.searchText.patchValue("");
});
diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts
index ae2e1ba97c..0452e9be83 100644
--- a/apps/desktop/src/app/services/init.service.ts
+++ b/apps/desktop/src/app/services/init.service.ts
@@ -1,10 +1,12 @@
import { DOCUMENT } from "@angular/common";
import { Inject, Injectable } from "@angular/core";
+import { firstValueFrom } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -12,9 +14,10 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
-import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service";
+import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
@@ -36,7 +39,8 @@ export class InitService {
private nativeMessagingService: NativeMessagingService,
private themingService: AbstractThemingService,
private encryptService: EncryptService,
- private userKeyInitService: UserKeyInitService,
+ private userAutoUnlockKeyService: UserAutoUnlockKeyService,
+ private accountService: AccountService,
@Inject(DOCUMENT) private document: Document,
) {}
@@ -44,7 +48,18 @@ export class InitService {
return async () => {
this.nativeMessagingService.init();
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
- this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
+
+ const accounts = await firstValueFrom(this.accountService.accounts$);
+ const setUserKeyInMemoryPromises = [];
+ for (const userId of Object.keys(accounts) as UserId[]) {
+ // For each acct, we must await the process of setting the user key in memory
+ // if the auto user key is set to avoid race conditions of any code trying to access
+ // the user key from mem.
+ setUserKeyInMemoryPromises.push(
+ this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId),
+ );
+ }
+ await Promise.all(setUserKeyInMemoryPromises);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts
index b888df8013..a485b925ba 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -15,6 +15,7 @@ import {
SafeInjectionToken,
STATE_FACTORY,
INTRAPROCESS_MESSAGING_SUBJECT,
+ CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@@ -25,6 +26,7 @@ import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/comm
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
+import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -57,7 +59,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components";
-import { LoginGuard } from "../../auth/guards/login.guard";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@@ -100,7 +101,6 @@ const safeProviders: SafeProvider[] = [
safeProvider(InitService),
safeProvider(NativeMessagingService),
safeProvider(SearchBarService),
- safeProvider(LoginGuard),
safeProvider(DialogService),
safeProvider({
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
@@ -190,6 +190,7 @@ const safeProviders: SafeProvider[] = [
AutofillSettingsServiceAbstraction,
VaultTimeoutSettingsService,
BiometricStateService,
+ AccountServiceAbstraction,
],
}),
safeProvider({
@@ -275,6 +276,10 @@ const safeProviders: SafeProvider[] = [
useClass: NativeMessagingManifestService,
deps: [],
}),
+ safeProvider({
+ provide: CLIENT_TYPE,
+ useValue: ClientType.Desktop,
+ }),
];
@NgModule({
diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts
index 51b5bf93a2..d908de8ef7 100644
--- a/apps/desktop/src/app/tools/generator.component.spec.ts
+++ b/apps/desktop/src/app/tools/generator.component.spec.ts
@@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -59,6 +60,10 @@ describe("GeneratorComponent", () => {
provide: CipherService,
useValue: mock(),
},
+ {
+ provide: AccountService,
+ useValue: mock(),
+ },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/apps/desktop/src/app/tools/send/add-edit.component.ts b/apps/desktop/src/app/tools/send/add-edit.component.ts
index 7bdd5efbba..804a390438 100644
--- a/apps/desktop/src/app/tools/send/add-edit.component.ts
+++ b/apps/desktop/src/app/tools/send/add-edit.component.ts
@@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent {
dialogService: DialogService,
formBuilder: FormBuilder,
billingAccountProfileStateService: BillingAccountProfileStateService,
+ accountService: AccountService,
) {
super(
i18nService,
@@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent {
dialogService,
formBuilder,
billingAccountProfileStateService,
+ accountService,
);
}
diff --git a/apps/desktop/src/auth/guards/login.guard.ts b/apps/desktop/src/auth/guards/login.guard.ts
deleted file mode 100644
index f6c67d5af9..0000000000
--- a/apps/desktop/src/auth/guards/login.guard.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Injectable } from "@angular/core";
-import { CanActivate } from "@angular/router";
-import { firstValueFrom } from "rxjs";
-
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
-
-const maxAllowedAccounts = 5;
-
-@Injectable()
-export class LoginGuard implements CanActivate {
- protected homepage = "vault";
- constructor(
- private stateService: StateService,
- private platformUtilsService: PlatformUtilsService,
- private i18nService: I18nService,
- ) {}
-
- async canActivate() {
- const accounts = await firstValueFrom(this.stateService.accounts$);
- if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached"));
- return false;
- }
-
- return true;
- }
-}
diff --git a/apps/desktop/src/auth/guards/max-accounts.guard.ts b/apps/desktop/src/auth/guards/max-accounts.guard.ts
new file mode 100644
index 0000000000..65c4ac99d0
--- /dev/null
+++ b/apps/desktop/src/auth/guards/max-accounts.guard.ts
@@ -0,0 +1,38 @@
+import { inject } from "@angular/core";
+import { CanActivateFn } from "@angular/router";
+import { Observable, map } from "rxjs";
+
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { ToastService } from "@bitwarden/components";
+
+const maxAllowedAccounts = 5;
+
+function maxAccountsGuard(): Observable {
+ const authService = inject(AuthService);
+ const toastService = inject(ToastService);
+ const i18nService = inject(I18nService);
+
+ return authService.authStatuses$.pipe(
+ map((statuses) =>
+ Object.values(statuses).filter((status) => status != AuthenticationStatus.LoggedOut),
+ ),
+ map((accounts) => {
+ if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) {
+ toastService.showToast({
+ variant: "error",
+ title: null,
+ message: i18nService.t("accountLimitReached"),
+ });
+ return false;
+ }
+
+ return true;
+ }),
+ );
+}
+
+export function maxAccountsGuardFn(): CanActivateFn {
+ return () => maxAccountsGuard();
+}
diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts
index f998e75d7a..2137b707f6 100644
--- a/apps/desktop/src/auth/lock.component.spec.ts
+++ b/apps/desktop/src/auth/lock.component.spec.ts
@@ -13,6 +13,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -50,7 +51,7 @@ describe("LockComponent", () => {
let component: LockComponent;
let fixture: ComponentFixture;
let stateServiceMock: MockProxy;
- const biometricStateService = mock();
+ let biometricStateService: MockProxy;
let messagingServiceMock: MockProxy;
let broadcasterServiceMock: MockProxy;
let platformUtilsServiceMock: MockProxy;
@@ -62,7 +63,6 @@ describe("LockComponent", () => {
beforeEach(async () => {
stateServiceMock = mock();
- stateServiceMock.activeAccount$ = of(null);
messagingServiceMock = mock();
broadcasterServiceMock = mock();
@@ -73,6 +73,7 @@ describe("LockComponent", () => {
mockMasterPasswordService = new FakeMasterPasswordService();
+ biometricStateService = mock();
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
biometricStateService.promptAutomatically$ = of(false);
biometricStateService.promptCancelled$ = of(false);
@@ -165,6 +166,10 @@ describe("LockComponent", () => {
provide: AccountService,
useValue: accountService,
},
+ {
+ provide: AuthService,
+ useValue: mock(),
+ },
{
provide: KdfConfigService,
useValue: mock(),
diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts
index 8e87b6663f..d95df419e1 100644
--- a/apps/desktop/src/auth/lock.component.ts
+++ b/apps/desktop/src/auth/lock.component.ts
@@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -64,6 +65,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService: PinCryptoServiceAbstraction,
biometricStateService: BiometricStateService,
accountService: AccountService,
+ authService: AuthService,
kdfConfigService: KdfConfigService,
) {
super(
@@ -89,6 +91,7 @@ export class LockComponent extends BaseLockComponent {
pinCryptoService,
biometricStateService,
accountService,
+ authService,
kdfConfigService,
);
}
diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json
index 86550b736f..e82420a1f5 100644
--- a/apps/desktop/src/locales/fr/messages.json
+++ b/apps/desktop/src/locales/fr/messages.json
@@ -1636,7 +1636,7 @@
"message": "Error enabling browser integration"
},
"browserIntegrationErrorDesc": {
- "message": "An error has occurred while enabling browser integration."
+ "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur."
},
"browserIntegrationMasOnlyDesc": {
"message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment."
@@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
- "message": "Success"
+ "message": "Succès"
},
"troubleshooting": {
"message": "Résolution de problèmes"
diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json
index 5c91fb4b94..838b3fc7c8 100644
--- a/apps/desktop/src/locales/hu/messages.json
+++ b/apps/desktop/src/locales/hu/messages.json
@@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
- "message": "Success"
+ "message": "Sikeres"
},
"troubleshooting": {
"message": "Hibaelhárítás"
diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json
index aa057f54ab..e2e068362e 100644
--- a/apps/desktop/src/locales/lv/messages.json
+++ b/apps/desktop/src/locales/lv/messages.json
@@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
- "message": "Success"
+ "message": "Izdevās"
},
"troubleshooting": {
"message": "Sarežģījumu novēršana"
diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json
index b5f2a413d6..f56572259b 100644
--- a/apps/desktop/src/locales/nl/messages.json
+++ b/apps/desktop/src/locales/nl/messages.json
@@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
- "message": "Success"
+ "message": "Succes"
},
"troubleshooting": {
"message": "Probleemoplossing"
diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json
index 9837be29e3..aad13e06ef 100644
--- a/apps/desktop/src/locales/zh_CN/messages.json
+++ b/apps/desktop/src/locales/zh_CN/messages.json
@@ -2698,7 +2698,7 @@
"description": "Label indicating the most common import formats"
},
"success": {
- "message": "Success"
+ "message": "成功"
},
"troubleshooting": {
"message": "故障排除"
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index da4c14b4aa..d11fceeacc 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -6,6 +6,7 @@ import { Subject, firstValueFrom } from "rxjs";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
+import { ClientType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -157,7 +158,7 @@ export class Main {
activeUserStateProvider,
singleUserStateProvider,
globalStateProvider,
- new DefaultDerivedStateProvider(storageServiceProvider),
+ new DefaultDerivedStateProvider(),
);
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
@@ -190,6 +191,7 @@ export class Main {
this.storageService,
this.logService,
new MigrationBuilderService(),
+ ClientType.Desktop,
);
// TODO: this state service will have access to on disk storage, but not in memory storage.
diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts
index eb1dacf825..b71774c5af 100644
--- a/apps/desktop/src/main/menu/menubar.ts
+++ b/apps/desktop/src/main/menu/menubar.ts
@@ -65,9 +65,10 @@ export class Menubar {
isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true;
}
- const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable;
+ const isLockable =
+ !isLocked && updateRequest?.accounts?.[updateRequest.activeUserId]?.isLockable;
const hasMasterPassword =
- updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false;
+ updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false;
this.items = [
new FileMenu(
diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts
index 771d25ef0a..d81d647652 100644
--- a/apps/desktop/src/platform/preload.ts
+++ b/apps/desktop/src/platform/preload.ts
@@ -103,7 +103,8 @@ export default {
isMacAppStore: isMacAppStore(),
isWindowsStore: isWindowsStore(),
reloadProcess: () => ipcRenderer.send("reload-process"),
- log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }),
+ log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
+ ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),
openContextMenu: (
menu: {
diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts
index d967e5fb1d..09ddad07c1 100644
--- a/apps/desktop/src/platform/services/desktop-settings.service.ts
+++ b/apps/desktop/src/platform/services/desktop-settings.service.ts
@@ -164,7 +164,7 @@ export class DesktopSettingsService {
/**
* Sets the setting for whether or not the application should be shown in the dock.
- * @param value `true` if the application should should in the dock, `false` if it should not.
+ * @param value `true` if the application should show in the dock, `false` if it should not.
*/
async setAlwaysShowDock(value: boolean) {
await this.alwaysShowDockState.update(() => value);
diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts
index 832365785c..0725de3dc9 100644
--- a/apps/desktop/src/platform/services/electron-log.main.service.ts
+++ b/apps/desktop/src/platform/services/electron-log.main.service.ts
@@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService {
}
log.initialize();
- ipcMain.handle("ipc.log", (_event, { level, message }) => {
- this.write(level, message);
+ ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => {
+ this.write(level, message, ...optionalParams);
});
}
- write(level: LogLevelType, message: string) {
+ write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
switch (level) {
case LogLevelType.Debug:
- log.debug(message);
+ log.debug(message, ...optionalParams);
break;
case LogLevelType.Info:
- log.info(message);
+ log.info(message, ...optionalParams);
break;
case LogLevelType.Warning:
- log.warn(message);
+ log.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
- log.error(message);
+ log.error(message, ...optionalParams);
break;
default:
break;
diff --git a/apps/desktop/src/platform/services/electron-log.renderer.service.ts b/apps/desktop/src/platform/services/electron-log.renderer.service.ts
index e0e0757e6a..cea939f160 100644
--- a/apps/desktop/src/platform/services/electron-log.renderer.service.ts
+++ b/apps/desktop/src/platform/services/electron-log.renderer.service.ts
@@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService {
super(ipc.platform.isDev, filter);
}
- write(level: LogLevelType, message: string) {
+ write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
if (this.filter != null && this.filter(level)) {
return;
}
/* eslint-disable no-console */
- ipc.platform.log(level, message).catch((e) => console.log("Error logging", e));
+ ipc.platform
+ .log(level, message, ...optionalParams)
+ .catch((e) => console.log("Error logging", e));
/* eslint-disable no-console */
switch (level) {
case LogLevelType.Debug:
- console.debug(message);
+ console.debug(message, ...optionalParams);
break;
case LogLevelType.Info:
- console.info(message);
+ console.info(message, ...optionalParams);
break;
case LogLevelType.Warning:
- console.warn(message);
+ console.warn(message, ...optionalParams);
break;
case LogLevelType.Error:
- console.error(message);
+ console.error(message, ...optionalParams);
break;
default:
break;
diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
index b1a84c22f3..7de0c98cd5 100644
--- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
+++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
@@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
FeatureFlag.ShowPaymentMethodWarningBanners,
- false,
);
constructor(
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
index f1af950650..a67bea39c0 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts
@@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy {
groups: groups$,
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
- false,
),
})
.pipe(takeUntil(this.destroy$))
@@ -620,7 +619,7 @@ export class MemberDialogComponent implements OnDestroy {
}
function mapCollectionToAccessItemView(
- collection: CollectionView,
+ collection: CollectionAdminView,
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
accessSelection?: CollectionAccessSelectionView,
@@ -632,7 +631,8 @@ function mapCollectionToAccessItemView(
labelName: collection.name,
listName: collection.name,
readonly:
- group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled),
+ group !== undefined ||
+ !collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
viaGroupName: group?.name,
};
diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts
index 1ce05f7a30..d8091e46ae 100644
--- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts
+++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts
@@ -44,12 +44,10 @@ export class AccountComponent {
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsMigration,
- false,
);
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
- false,
);
// FormGroup validators taken from server Organization domain object
diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts
index abf1d249e1..80d77968f2 100644
--- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts
+++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts
@@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
@@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../..
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
tabbedHeader = false;
constructor(
+ dialogService: DialogService,
apiService: ApiService,
modalService: ModalService,
messagingService: MessagingService,
@@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
+ dialogService,
apiService,
modalService,
messagingService,
diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts
index 1da2d94c15..1939bb11f5 100644
--- a/apps/web/src/app/app.component.ts
+++ b/apps/web/src/app/app.component.ts
@@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
-import { Subject, switchMap, takeUntil, timer } from "rxjs";
+import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
@@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
@@ -51,7 +52,7 @@ const PaymentMethodWarningsRefresh = 60000; // 1 Minute
templateUrl: "app.component.html",
})
export class AppComponent implements OnDestroy, OnInit {
- private lastActivity: number = null;
+ private lastActivity: Date = null;
private idleTimer: number = null;
private isIdle = false;
private destroy$ = new Subject();
@@ -86,6 +87,7 @@ export class AppComponent implements OnDestroy, OnInit {
private stateEventRunnerService: StateEventRunnerService,
private paymentMethodWarningService: PaymentMethodWarningService,
private organizationService: InternalOrganizationServiceAbstraction,
+ private accountService: AccountService,
) {}
ngOnInit() {
@@ -298,15 +300,16 @@ export class AppComponent implements OnDestroy, OnInit {
}
private async recordActivity() {
- const now = new Date().getTime();
- if (this.lastActivity != null && now - this.lastActivity < 250) {
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+ const now = new Date();
+ if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.stateService.setLastActive(now);
+ await this.accountService.setAccountActivity(activeUserId, now);
// Idle states
if (this.isIdle) {
this.isIdle = false;
diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts
index 94c6208115..dc5f933724 100644
--- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts
+++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts
@@ -90,7 +90,7 @@ export class UserKeyRotationService {
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
- if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
+ if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
await this.apiService.postUserKeyUpdate(request);
} else {
await this.rotateUserKeyAndEncryptedDataLegacy(request);
diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html
index 33bf4fb130..e17714cca7 100644
--- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html
+++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html
@@ -15,13 +15,6 @@
×
-
-
-
-
-
-