Merge branch 'main' into SM-1094-Promises
This commit is contained in:
commit
a997f04542
|
@ -164,6 +164,10 @@ jobs:
|
||||||
run: npm run dist:mv3
|
run: npm run dist:mv3
|
||||||
working-directory: browser-source/apps/browser
|
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
|
- name: Gulp
|
||||||
run: gulp ci
|
run: gulp ci
|
||||||
working-directory: browser-source/apps/browser
|
working-directory: browser-source/apps/browser
|
||||||
|
@ -196,6 +200,13 @@ jobs:
|
||||||
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
||||||
if-no-files-found: error
|
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
|
- name: Upload Firefox artifact
|
||||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -230,6 +230,17 @@ jobs:
|
||||||
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
||||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
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:
|
azure-deploy:
|
||||||
name: Deploy Web Vault to ${{ inputs.environment }} Storage Account
|
name: Deploy Web Vault to ${{ inputs.environment }} Storage Account
|
||||||
needs:
|
needs:
|
||||||
|
|
|
@ -9,6 +9,8 @@ const config: StorybookConfig = {
|
||||||
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
|
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
"../apps/web/src/**/*.mdx",
|
"../apps/web/src/**/*.mdx",
|
||||||
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
"../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/**/*.mdx",
|
||||||
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
{
|
{
|
||||||
"extends": "../tsconfig",
|
"extends": "../tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node", "jest", "chrome"],
|
|
||||||
"allowSyntheticDefaultImports": true
|
"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": [
|
"files": [
|
||||||
"./typings.d.ts",
|
|
||||||
"./preview.tsx",
|
"./preview.tsx",
|
||||||
"../libs/components/src/main.ts",
|
"../libs/components/src/main.ts",
|
||||||
"../libs/components/src/polyfills.ts"
|
"../libs/components/src/polyfills.ts"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
declare module "*.md" {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
|
@ -30,11 +30,27 @@ const filters = {
|
||||||
safari: ["!build/safari/**/*"],
|
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() {
|
function buildString() {
|
||||||
var build = "";
|
var build = "";
|
||||||
if (process.env.MANIFEST_VERSION) {
|
if (process.env.MANIFEST_VERSION) {
|
||||||
build = `-mv${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 !== "") {
|
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
|
||||||
build = `-${process.env.BUILD_NUMBER}`;
|
build = `-${process.env.BUILD_NUMBER}`;
|
||||||
}
|
}
|
||||||
|
@ -65,6 +81,9 @@ function distFirefox() {
|
||||||
manifest.optional_permissions = manifest.optional_permissions.filter(
|
manifest.optional_permissions = manifest.optional_permissions.filter(
|
||||||
(permission) => permission !== "privacy",
|
(permission) => permission !== "privacy",
|
||||||
);
|
);
|
||||||
|
if (process.env.BETA_BUILD === "1") {
|
||||||
|
manifest = applyBetaLabels(manifest);
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -72,6 +91,9 @@ function distFirefox() {
|
||||||
function distOpera() {
|
function distOpera() {
|
||||||
return dist("opera", (manifest) => {
|
return dist("opera", (manifest) => {
|
||||||
delete manifest.applications;
|
delete manifest.applications;
|
||||||
|
if (process.env.BETA_BUILD === "1") {
|
||||||
|
manifest = applyBetaLabels(manifest);
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -81,6 +103,9 @@ function distChrome() {
|
||||||
delete manifest.applications;
|
delete manifest.applications;
|
||||||
delete manifest.sidebar_action;
|
delete manifest.sidebar_action;
|
||||||
delete manifest.commands._execute_sidebar_action;
|
delete manifest.commands._execute_sidebar_action;
|
||||||
|
if (process.env.BETA_BUILD === "1") {
|
||||||
|
manifest = applyBetaLabels(manifest);
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -90,6 +115,9 @@ function distEdge() {
|
||||||
delete manifest.applications;
|
delete manifest.applications;
|
||||||
delete manifest.sidebar_action;
|
delete manifest.sidebar_action;
|
||||||
delete manifest.commands._execute_sidebar_action;
|
delete manifest.commands._execute_sidebar_action;
|
||||||
|
if (process.env.BETA_BUILD === "1") {
|
||||||
|
manifest = applyBetaLabels(manifest);
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -210,6 +238,9 @@ async function safariCopyBuild(source, dest) {
|
||||||
delete manifest.commands._execute_sidebar_action;
|
delete manifest.commands._execute_sidebar_action;
|
||||||
delete manifest.optional_permissions;
|
delete manifest.optional_permissions;
|
||||||
manifest.permissions.push("nativeMessaging");
|
manifest.permissions.push("nativeMessaging");
|
||||||
|
if (process.env.BETA_BUILD === "1") {
|
||||||
|
manifest = applyBetaLabels(manifest);
|
||||||
|
}
|
||||||
return manifest;
|
return manifest;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -235,6 +266,30 @@ async function ciCoverage(cb) {
|
||||||
.pipe(gulp.dest(paths.coverage));
|
.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:firefox"] = distFirefox;
|
||||||
exports["dist:chrome"] = distChrome;
|
exports["dist:chrome"] = distChrome;
|
||||||
exports["dist:opera"] = distOpera;
|
exports["dist:opera"] = distOpera;
|
||||||
|
|
|
@ -7,10 +7,14 @@
|
||||||
"build:watch": "webpack --watch",
|
"build:watch": "webpack --watch",
|
||||||
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
|
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
|
||||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
"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",
|
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
||||||
"dist": "npm run build:prod && gulp dist",
|
"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": "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": "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:firefox": "npm run build:prod && gulp dist:firefox",
|
||||||
"dist:opera": "npm run build:prod && gulp dist:opera",
|
"dist:opera": "npm run build:prod && gulp dist:opera",
|
||||||
"dist:safari": "npm run build:prod && gulp dist:safari",
|
"dist:safari": "npm run build:prod && gulp dist:safari",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"extDesc": {
|
"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)"
|
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"loginOrCreateNewAccount": {
|
"loginOrCreateNewAccount": {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"extDesc": {
|
"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)"
|
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"loginOrCreateNewAccount": {
|
"loginOrCreateNewAccount": {
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
"message": "Bitwarden"
|
"message": "Bitwarden"
|
||||||
},
|
},
|
||||||
"extName": {
|
"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)"
|
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"extDesc": {
|
"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)"
|
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||||
},
|
},
|
||||||
"loginOrCreateNewAccount": {
|
"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."
|
"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": {
|
"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": {
|
"showCardsCurrentTab": {
|
||||||
"message": "Hiển thị thẻ trên trang Tab"
|
"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."
|
"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": {
|
"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": {
|
"enableUsePasskeys": {
|
||||||
"message": "Ask to save and use passkeys"
|
"message": "Đưa ra lựa chọn để lưu và sử dụng passkey"
|
||||||
},
|
},
|
||||||
"usePasskeysDesc": {
|
"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": {
|
"notificationChangeDesc": {
|
||||||
"message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?"
|
"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. "
|
"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": {
|
"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": {
|
"defaultUriMatchDetection": {
|
||||||
"message": "Phương thức kiểm tra URI mặc định",
|
"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."
|
"message": "Thay đổi màu sắc ứng dụng."
|
||||||
},
|
},
|
||||||
"themeDescAlt": {
|
"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": {
|
"dark": {
|
||||||
"message": "Tối",
|
"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."
|
"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": {
|
"turnOffBrowserBuiltInPasswordManagerSettingsLink": {
|
||||||
"message": "Edit browser settings."
|
"message": "Thay đổi cài đặt của trình duyệt."
|
||||||
},
|
},
|
||||||
"autofillOverlayVisibilityOff": {
|
"autofillOverlayVisibilityOff": {
|
||||||
"message": "Off",
|
"message": "Tắt",
|
||||||
"description": "Overlay setting select option for disabling autofill overlay"
|
"description": "Overlay setting select option for disabling autofill overlay"
|
||||||
},
|
},
|
||||||
"autofillOverlayVisibilityOnFieldFocus": {
|
"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."
|
"message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập."
|
||||||
},
|
},
|
||||||
"faviconDescAlt": {
|
"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": {
|
"enableBadgeCounter": {
|
||||||
"message": "Hiển thị biểu tượng bộ đếm"
|
"message": "Hiển thị biểu tượng bộ đếm"
|
||||||
|
@ -1500,7 +1500,7 @@
|
||||||
"message": "Mã PIN không hợp lệ."
|
"message": "Mã PIN không hợp lệ."
|
||||||
},
|
},
|
||||||
"tooManyInvalidPinEntryAttemptsLoggingOut": {
|
"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": {
|
"unlockWithBiometrics": {
|
||||||
"message": "Mở khóa bằng sinh trắc học"
|
"message": "Mở khóa bằng sinh trắc học"
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
|
||||||
(click)="lock()"
|
(click)="lock(currentAccount.id)"
|
||||||
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
|
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
|
||||||
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
|
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
|
||||||
>
|
>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
|
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
|
||||||
(click)="logOut()"
|
(click)="logOut(currentAccount.id)"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
|
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
|
||||||
{{ "logOut" | i18n }}
|
{{ "logOut" | i18n }}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { AccountSwitcherService } from "./services/account-switcher.service";
|
import { AccountSwitcherService } from "./services/account-switcher.service";
|
||||||
|
@ -64,9 +65,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
async lock(userId?: string) {
|
async lock(userId: string) {
|
||||||
this.loading = true;
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate(["lock"]);
|
this.router.navigate(["lock"]);
|
||||||
|
@ -96,7 +97,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
.subscribe(() => this.router.navigate(["lock"]));
|
.subscribe(() => this.router.navigate(["lock"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async logOut() {
|
async logOut(userId: UserId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "logOut" },
|
title: { key: "logOut" },
|
||||||
|
@ -105,7 +106,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (confirmed) {
|
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.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
|
|
@ -58,6 +58,7 @@ describe("AccountSwitcherService", () => {
|
||||||
const accountInfo: AccountInfo = {
|
const accountInfo: AccountInfo = {
|
||||||
name: "Test User 1",
|
name: "Test User 1",
|
||||||
email: "test1@email.com",
|
email: "test1@email.com",
|
||||||
|
emailVerified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||||
|
@ -89,6 +90,7 @@ describe("AccountSwitcherService", () => {
|
||||||
for (let i = 0; i < numberOfAccounts; i++) {
|
for (let i = 0; i < numberOfAccounts; i++) {
|
||||||
seedAccounts[`${i}` as UserId] = {
|
seedAccounts[`${i}` as UserId] = {
|
||||||
email: `test${i}@email.com`,
|
email: `test${i}@email.com`,
|
||||||
|
emailVerified: true,
|
||||||
name: "Test User ${i}",
|
name: "Test User ${i}",
|
||||||
};
|
};
|
||||||
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
||||||
|
@ -113,6 +115,7 @@ describe("AccountSwitcherService", () => {
|
||||||
const user1AccountInfo: AccountInfo = {
|
const user1AccountInfo: AccountInfo = {
|
||||||
name: "Test User 1",
|
name: "Test User 1",
|
||||||
email: "",
|
email: "",
|
||||||
|
emailVerified: true,
|
||||||
};
|
};
|
||||||
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
||||||
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
||||||
|
|
|
@ -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 }]>(
|
this.switchAccountFinished$ = fromChromeEvent<[message: { command: string; userId: string }]>(
|
||||||
chrome.runtime.onMessage,
|
chrome.runtime.onMessage,
|
||||||
).pipe(
|
).pipe(
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
policyApiService: PolicyApiServiceAbstraction,
|
policyApiService: PolicyApiServiceAbstraction,
|
||||||
policyService: InternalPolicyService,
|
policyService: InternalPolicyService,
|
||||||
passwordStrengthService: PasswordStrengthServiceAbstraction,
|
passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||||
private authService: AuthService,
|
authService: AuthService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
|
@ -92,6 +92,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
accountService,
|
accountService,
|
||||||
|
authService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
);
|
);
|
||||||
this.successRoute = "/tabs/current";
|
this.successRoute = "/tabs/current";
|
||||||
|
|
|
@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
|
||||||
|
|
||||||
export default class WebRequestBackground {
|
export default class WebRequestBackground {
|
||||||
private pendingAuthRequests: any[] = [];
|
private pendingAuthRequests: Set<string> = new Set<string>([]);
|
||||||
private webRequest: any;
|
|
||||||
private isFirefox: boolean;
|
private isFirefox: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private readonly webRequest: typeof chrome.webRequest,
|
||||||
) {
|
) {
|
||||||
if (BrowserApi.isManifestVersion(2)) {
|
|
||||||
this.webRequest = chrome.webRequest;
|
|
||||||
}
|
|
||||||
this.isFirefox = platformUtilsService.isFirefox();
|
this.isFirefox = platformUtilsService.isFirefox();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
startListening() {
|
||||||
if (!this.webRequest || !this.webRequest.onAuthRequired) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.webRequest.onAuthRequired.addListener(
|
this.webRequest.onAuthRequired.addListener(
|
||||||
async (details: any, callback: any) => {
|
async (details, callback) => {
|
||||||
if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
|
if (!details.url || this.pendingAuthRequests.has(details.requestId)) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
callback(null);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.pendingAuthRequests.add(details.requestId);
|
||||||
this.pendingAuthRequests.push(details.requestId);
|
|
||||||
|
|
||||||
if (this.isFirefox) {
|
if (this.isFirefox) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
@ -51,7 +40,7 @@ export default class WebRequestBackground {
|
||||||
[this.isFirefox ? "blocking" : "asyncBlocking"],
|
[this.isFirefox ? "blocking" : "asyncBlocking"],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
|
this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), {
|
||||||
urls: ["http://*/*"],
|
urls: ["http://*/*"],
|
||||||
});
|
});
|
||||||
this.webRequest.onErrorOccurred.addListener(
|
this.webRequest.onErrorOccurred.addListener(
|
||||||
|
@ -91,10 +80,7 @@ export default class WebRequestBackground {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private completeAuthRequest(details: any) {
|
private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) {
|
||||||
const i = this.pendingAuthRequests.indexOf(details.requestId);
|
this.pendingAuthRequests.delete(details.requestId);
|
||||||
if (i > -1) {
|
|
||||||
this.pendingAuthRequests.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ import {
|
||||||
GENERATE_PASSWORD_ID,
|
GENERATE_PASSWORD_ID,
|
||||||
NOOP_COMMAND_SUFFIX,
|
NOOP_COMMAND_SUFFIX,
|
||||||
} from "@bitwarden/common/autofill/constants";
|
} 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
@ -65,7 +66,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||||
let autofill: AutofillAction;
|
let autofill: AutofillAction;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let cipherService: MockProxy<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let stateService: MockProxy<StateService>;
|
let accountService: FakeAccountService;
|
||||||
let totpService: MockProxy<TotpService>;
|
let totpService: MockProxy<TotpService>;
|
||||||
let eventCollectionService: MockProxy<EventCollectionService>;
|
let eventCollectionService: MockProxy<EventCollectionService>;
|
||||||
let userVerificationService: MockProxy<UserVerificationService>;
|
let userVerificationService: MockProxy<UserVerificationService>;
|
||||||
|
@ -78,7 +79,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||||
autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>();
|
autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>();
|
||||||
authService = mock();
|
authService = mock();
|
||||||
cipherService = mock();
|
cipherService = mock();
|
||||||
stateService = mock();
|
accountService = mockAccountServiceWith("userId" as UserId);
|
||||||
totpService = mock();
|
totpService = mock();
|
||||||
eventCollectionService = mock();
|
eventCollectionService = mock();
|
||||||
|
|
||||||
|
@ -88,10 +89,10 @@ describe("ContextMenuClickedHandler", () => {
|
||||||
autofill,
|
autofill,
|
||||||
authService,
|
authService,
|
||||||
cipherService,
|
cipherService,
|
||||||
stateService,
|
|
||||||
totpService,
|
totpService,
|
||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
@ -17,7 +20,6 @@ import {
|
||||||
NOOP_COMMAND_SUFFIX,
|
NOOP_COMMAND_SUFFIX,
|
||||||
} from "@bitwarden/common/autofill/constants";
|
} from "@bitwarden/common/autofill/constants";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
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 { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
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 { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
||||||
import {
|
import {
|
||||||
authServiceFactory,
|
authServiceFactory,
|
||||||
AuthServiceInitOptions,
|
AuthServiceInitOptions,
|
||||||
|
@ -37,7 +40,6 @@ import { autofillSettingsServiceFactory } from "../../autofill/background/servic
|
||||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { CachedServices } from "../../platform/background/service-factories/factory-options";
|
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 { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory";
|
import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory";
|
||||||
import {
|
import {
|
||||||
|
@ -71,10 +73,10 @@ export class ContextMenuClickedHandler {
|
||||||
private autofillAction: AutofillAction,
|
private autofillAction: AutofillAction,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private stateService: StateService,
|
|
||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static async mv3Create(cachedServices: CachedServices) {
|
static async mv3Create(cachedServices: CachedServices) {
|
||||||
|
@ -128,10 +130,10 @@ export class ContextMenuClickedHandler {
|
||||||
(tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher),
|
(tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher),
|
||||||
await authServiceFactory(cachedServices, serviceOptions),
|
await authServiceFactory(cachedServices, serviceOptions),
|
||||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
await cipherServiceFactory(cachedServices, serviceOptions),
|
||||||
await stateServiceFactory(cachedServices, serviceOptions),
|
|
||||||
await totpServiceFactory(cachedServices, serviceOptions),
|
await totpServiceFactory(cachedServices, serviceOptions),
|
||||||
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
||||||
await userVerificationServiceFactory(cachedServices, serviceOptions),
|
await userVerificationServiceFactory(cachedServices, serviceOptions),
|
||||||
|
await accountServiceFactory(cachedServices, serviceOptions),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,9 +241,10 @@ export class ContextMenuClickedHandler {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
const activeUserId = await firstValueFrom(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
this.stateService.setLastActive(new Date().getTime());
|
);
|
||||||
|
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||||
switch (info.parentMenuItemId) {
|
switch (info.parentMenuItemId) {
|
||||||
case AUTOFILL_ID:
|
case AUTOFILL_ID:
|
||||||
case AUTOFILL_IDENTITY_ID:
|
case AUTOFILL_IDENTITY_ID:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Subject, firstValueFrom, merge } from "rxjs";
|
import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PinCryptoServiceAbstraction,
|
PinCryptoServiceAbstraction,
|
||||||
|
@ -71,6 +71,7 @@ import {
|
||||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { 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 { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
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 { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
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 { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import {
|
import {
|
||||||
ActiveUserStateProvider,
|
ActiveUserStateProvider,
|
||||||
|
@ -333,7 +334,7 @@ export default class MainBackground {
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
|
||||||
intraprocessMessagingSubject: Subject<Message<object>>;
|
intraprocessMessagingSubject: Subject<Message<object>>;
|
||||||
userKeyInitService: UserKeyInitService;
|
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||||
scriptInjectorService: BrowserScriptInjectorService;
|
scriptInjectorService: BrowserScriptInjectorService;
|
||||||
kdfConfigService: kdfConfigServiceAbstraction;
|
kdfConfigService: kdfConfigServiceAbstraction;
|
||||||
|
|
||||||
|
@ -490,7 +491,7 @@ export default class MainBackground {
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
|
this.derivedStateProvider = new BackgroundDerivedStateProvider();
|
||||||
this.stateProvider = new DefaultStateProvider(
|
this.stateProvider = new DefaultStateProvider(
|
||||||
this.activeUserStateProvider,
|
this.activeUserStateProvider,
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
|
@ -520,6 +521,7 @@ export default class MainBackground {
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
new MigrationBuilderService(),
|
new MigrationBuilderService(),
|
||||||
|
ClientType.Browser,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.stateService = new DefaultBrowserStateService(
|
this.stateService = new DefaultBrowserStateService(
|
||||||
|
@ -900,6 +902,7 @@ export default class MainBackground {
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
this.biometricStateService,
|
this.biometricStateService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Other fields
|
// Other fields
|
||||||
|
@ -918,7 +921,6 @@ export default class MainBackground {
|
||||||
this.autofillService,
|
this.autofillService,
|
||||||
this.platformUtilsService as BrowserPlatformUtilsService,
|
this.platformUtilsService as BrowserPlatformUtilsService,
|
||||||
this.notificationsService,
|
this.notificationsService,
|
||||||
this.stateService,
|
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.systemService,
|
this.systemService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
|
@ -927,6 +929,7 @@ export default class MainBackground {
|
||||||
this.configService,
|
this.configService,
|
||||||
this.fido2Background,
|
this.fido2Background,
|
||||||
messageListener,
|
messageListener,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||||
this.accountService,
|
this.accountService,
|
||||||
|
@ -1016,10 +1019,10 @@ export default class MainBackground {
|
||||||
},
|
},
|
||||||
this.authService,
|
this.authService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.stateService,
|
|
||||||
this.totpService,
|
this.totpService,
|
||||||
this.eventCollectionService,
|
this.eventCollectionService,
|
||||||
this.userVerificationService,
|
this.userVerificationService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
||||||
|
@ -1053,20 +1056,17 @@ export default class MainBackground {
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (BrowserApi.isManifestVersion(2)) {
|
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
|
||||||
this.webRequestBackground = new WebRequestBackground(
|
this.webRequestBackground = new WebRequestBackground(
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.authService,
|
this.authService,
|
||||||
|
chrome.webRequest,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userKeyInitService = new UserKeyInitService(
|
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||||
this.accountService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.logService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrap() {
|
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
|
// This is here instead of in in the InitService b/c we don't plan for
|
||||||
// side effects to run in the Browser InitService.
|
// 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();
|
await (this.i18nService as I18nService).init();
|
||||||
(this.eventUploadService as EventUploadService).init(true);
|
(this.eventUploadService as EventUploadService).init(true);
|
||||||
|
@ -1096,9 +1107,7 @@ export default class MainBackground {
|
||||||
await this.tabsBackground.init();
|
await this.tabsBackground.init();
|
||||||
this.contextMenusBackground?.init();
|
this.contextMenusBackground?.init();
|
||||||
await this.idleBackground.init();
|
await this.idleBackground.init();
|
||||||
if (BrowserApi.isManifestVersion(2)) {
|
this.webRequestBackground?.startListening();
|
||||||
await this.webRequestBackground.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
|
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {
|
||||||
// Set Private Mode windows to the default icon - they do not share state with the background page
|
// 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) {
|
async switchAccount(userId: UserId) {
|
||||||
try {
|
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) {
|
if (userId == null) {
|
||||||
this.loginEmailService.setRememberEmail(false);
|
this.loginEmailService.setRememberEmail(false);
|
||||||
|
@ -1196,7 +1210,18 @@ export default class MainBackground {
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(expired: boolean, userId?: UserId) {
|
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);
|
await this.eventUploadService.uploadEvents(userId as UserId);
|
||||||
|
|
||||||
|
@ -1220,7 +1245,11 @@ export default class MainBackground {
|
||||||
//Needs to be checked before state is cleaned
|
//Needs to be checked before state is cleaned
|
||||||
const needStorageReseed = await this.needsStorageReseed();
|
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);
|
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { firstValueFrom, mergeMap } from "rxjs";
|
import { firstValueFrom, map, mergeMap } from "rxjs";
|
||||||
|
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
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 { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
|
||||||
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
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 { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background";
|
import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background";
|
||||||
|
@ -37,7 +37,6 @@ export default class RuntimeBackground {
|
||||||
private autofillService: AutofillService,
|
private autofillService: AutofillService,
|
||||||
private platformUtilsService: BrowserPlatformUtilsService,
|
private platformUtilsService: BrowserPlatformUtilsService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private stateService: BrowserStateService,
|
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private systemService: SystemService,
|
private systemService: SystemService,
|
||||||
private environmentService: BrowserEnvironmentService,
|
private environmentService: BrowserEnvironmentService,
|
||||||
|
@ -46,6 +45,7 @@ export default class RuntimeBackground {
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private fido2Background: Fido2Background,
|
private fido2Background: Fido2Background,
|
||||||
private messageListener: MessageListener,
|
private messageListener: MessageListener,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||||
|
@ -76,7 +76,8 @@ export default class RuntimeBackground {
|
||||||
|
|
||||||
void this.processMessageWithSender(msg, sender).catch((err) =>
|
void this.processMessageWithSender(msg, sender).catch((err) =>
|
||||||
this.logService.error(
|
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;
|
return false;
|
||||||
|
@ -85,7 +86,11 @@ export default class RuntimeBackground {
|
||||||
this.messageListener.allMessages$
|
this.messageListener.allMessages$
|
||||||
.pipe(
|
.pipe(
|
||||||
mergeMap(async (message: any) => {
|
mergeMap(async (message: any) => {
|
||||||
await this.processMessage(message);
|
try {
|
||||||
|
await this.processMessage(message);
|
||||||
|
} catch (err) {
|
||||||
|
this.logService.error(err);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
@ -107,9 +112,10 @@ export default class RuntimeBackground {
|
||||||
switch (msg.sender) {
|
switch (msg.sender) {
|
||||||
case "autofiller":
|
case "autofiller":
|
||||||
case "autofill_cmd": {
|
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.
|
const activeUserId = await firstValueFrom(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
this.stateService.setLastActive(new Date().getTime());
|
);
|
||||||
|
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||||
const totpCode = await this.autofillService.doAutoFillActiveTab(
|
const totpCode = await this.autofillService.doAutoFillActiveTab(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
|
@ -60,7 +60,9 @@
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"idle",
|
"idle",
|
||||||
"scripting",
|
"scripting",
|
||||||
"offscreen"
|
"offscreen",
|
||||||
|
"webRequest",
|
||||||
|
"webRequestAuthProvider"
|
||||||
],
|
],
|
||||||
"optional_permissions": ["nativeMessaging", "privacy"],
|
"optional_permissions": ["nativeMessaging", "privacy"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
|
|
|
@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
||||||
|
|
||||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||||
import {
|
|
||||||
StorageServiceProviderInitOptions,
|
|
||||||
storageServiceProviderFactory,
|
|
||||||
} from "./storage-service-provider.factory";
|
|
||||||
|
|
||||||
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
|
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
|
||||||
StorageServiceProviderInitOptions;
|
|
||||||
|
|
||||||
export async function derivedStateProviderFactory(
|
export async function derivedStateProviderFactory(
|
||||||
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
||||||
|
@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
|
||||||
cache,
|
cache,
|
||||||
"derivedStateProvider",
|
"derivedStateProvider",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () => new BackgroundDerivedStateProvider(),
|
||||||
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ export async function migrationRunnerFactory(
|
||||||
await diskStorageServiceFactory(cache, opts),
|
await diskStorageServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
new MigrationBuilderService(),
|
new MigrationBuilderService(),
|
||||||
|
ClientType.Browser,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,10 +238,6 @@ export class BrowserApi {
|
||||||
return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage();
|
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
|
* 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
|
* available within background service worker. As a result, it will return an
|
||||||
|
|
|
@ -28,6 +28,7 @@ describe("OffscreenDocument", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a console message if the handler throws an error", async () => {
|
it("shows a console message if the handler throws an error", async () => {
|
||||||
|
const error = new Error("test error");
|
||||||
browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error"));
|
browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error"));
|
||||||
|
|
||||||
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" });
|
sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" });
|
||||||
|
@ -35,7 +36,8 @@ describe("OffscreenDocument", () => {
|
||||||
|
|
||||||
expect(browserClipboardServiceCopySpy).toHaveBeenCalled();
|
expect(browserClipboardServiceCopySpy).toHaveBeenCalled();
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
"Error resolving extension message response: Error: test error",
|
"Error resolving extension message response",
|
||||||
|
error,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface {
|
||||||
Promise.resolve(messageResponse)
|
Promise.resolve(messageResponse)
|
||||||
.then((response) => sendResponse(response))
|
.then((response) => sendResponse(response))
|
||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
this.consoleLogService.error(`Error resolving extension message response: ${error}`),
|
this.consoleLogService.error("Error resolving extension message response", error),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -203,7 +203,7 @@ describe("BrowserPopupUtils", () => {
|
||||||
expect(BrowserPopupUtils["buildPopoutUrl"]).not.toHaveBeenCalled();
|
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";
|
const url = "popup/index.html?uilocation=sidebar#/tabs/vault";
|
||||||
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Component, Input } from "@angular/core";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../flags";
|
import { enableAccountSwitching } from "../flags";
|
||||||
|
|
||||||
|
@ -16,18 +14,15 @@ export class HeaderComponent {
|
||||||
@Input() noTheme = false;
|
@Input() noTheme = false;
|
||||||
@Input() hideAccountSwitcher = false;
|
@Input() hideAccountSwitcher = false;
|
||||||
authedAccounts$: Observable<boolean>;
|
authedAccounts$: Observable<boolean>;
|
||||||
constructor(accountService: AccountService, authService: AuthService) {
|
constructor(authService: AuthService) {
|
||||||
this.authedAccounts$ = accountService.accounts$.pipe(
|
this.authedAccounts$ = authService.authStatuses$.pipe(
|
||||||
switchMap((accounts) => {
|
map((record) => Object.values(record)),
|
||||||
|
switchMap((statuses) => {
|
||||||
if (!enableAccountSwitching()) {
|
if (!enableAccountSwitching()) {
|
||||||
return of(false);
|
return of(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLatest(
|
return of(statuses.some((status) => status !== AuthenticationStatus.LoggedOut));
|
||||||
Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)),
|
|
||||||
).pipe(
|
|
||||||
map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<footer
|
||||||
|
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
||||||
|
>
|
||||||
|
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||||
|
<div class="tw-flex tw-justify-start tw-gap-2">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "popup-footer",
|
||||||
|
templateUrl: "popup-footer.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
})
|
||||||
|
export class PopupFooterComponent {}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<header
|
||||||
|
class="tw-p-4 tw-border-0 tw-border-solid tw-border-b tw-border-secondary-300 tw-bg-background"
|
||||||
|
>
|
||||||
|
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||||
|
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-back"
|
||||||
|
type="button"
|
||||||
|
*ngIf="showBackButton"
|
||||||
|
[title]="'back' | i18n"
|
||||||
|
[ariaLabel]="'back' | i18n"
|
||||||
|
></button>
|
||||||
|
<h1 bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">{{ pageTitle }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { Meta, Story, Canvas } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import * as stories from "./popup-layout.stories";
|
||||||
|
|
||||||
|
<Meta of={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-tab-navigation>
|
||||||
|
<popup-page></popup-page>
|
||||||
|
</popup-tab-navigation>
|
||||||
|
```
|
||||||
|
|
||||||
|
# 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
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header"></popup-header>
|
||||||
|
<div>This is content</div>
|
||||||
|
<popup-footer slot="footer"></popup-footer>
|
||||||
|
</popup-page>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
<popup-header pageTitle="Test" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<button></button>
|
||||||
|
<button></button>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
<popup-footer>
|
||||||
|
<button bitButton buttonType="primary">Save</button>
|
||||||
|
<button bitButton buttonType="secondary">Cancel</button>
|
||||||
|
</popup-footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.PopupTabNavigation} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Extension Page
|
||||||
|
|
||||||
|
Examples of using just the `popup-page` component, without and with a footer.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.PopupPage} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.PopupPageWithFooter} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Popped out
|
||||||
|
|
||||||
|
When the browser extension is popped out, the "popout" button should not be passed to the header.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.PoppedOut} />
|
||||||
|
</Canvas>
|
|
@ -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: `
|
||||||
|
<div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class ExtensionContainerComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-placeholder",
|
||||||
|
template: `
|
||||||
|
<bit-item-group aria-label="Mock Vault Items">
|
||||||
|
<bit-item *ngFor="let item of data; index as i">
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
{{ i }} of {{ data.length - 1 }}
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone" aria-label="Copy item"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="More options"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule],
|
||||||
|
})
|
||||||
|
class VaultComponent {
|
||||||
|
protected data = Array.from(Array(20).keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "generator-placeholder",
|
||||||
|
template: ` <div class="tw-text-main">generator stuff here</div> `,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class GeneratorComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "send-placeholder",
|
||||||
|
template: ` <div class="tw-text-main">send some stuff</div> `,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class SendComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "settings-placeholder",
|
||||||
|
template: ` <div class="tw-text-main">change your settings</div> `,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
class SettingsComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-add-button",
|
||||||
|
template: `
|
||||||
|
<button bitButton buttonType="primary" type="button">
|
||||||
|
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [ButtonModule],
|
||||||
|
})
|
||||||
|
class MockAddButtonComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-popout-button",
|
||||||
|
template: `
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-popout"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
title="Pop out"
|
||||||
|
aria-label="Pop out"
|
||||||
|
></button>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [IconButtonModule],
|
||||||
|
})
|
||||||
|
class MockPopoutButtonComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-current-account",
|
||||||
|
template: `
|
||||||
|
<button class="tw-bg-transparent tw-border-none" type="button">
|
||||||
|
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [AvatarModule],
|
||||||
|
})
|
||||||
|
class MockCurrentAccountComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-vault-page",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-add-button></mock-add-button>
|
||||||
|
<mock-popout-button></mock-popout-button>
|
||||||
|
<mock-current-account></mock-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<vault-placeholder></vault-placeholder>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
MockAddButtonComponent,
|
||||||
|
MockPopoutButtonComponent,
|
||||||
|
MockCurrentAccountComponent,
|
||||||
|
VaultComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MockVaultPageComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-vault-page-popped",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-add-button></mock-add-button>
|
||||||
|
<mock-current-account></mock-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<vault-placeholder></vault-placeholder>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
MockAddButtonComponent,
|
||||||
|
MockPopoutButtonComponent,
|
||||||
|
MockCurrentAccountComponent,
|
||||||
|
VaultComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MockVaultPagePoppedComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-generator-page",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-add-button></mock-add-button>
|
||||||
|
<mock-popout-button></mock-popout-button>
|
||||||
|
<mock-current-account></mock-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<generator-placeholder></generator-placeholder>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
MockAddButtonComponent,
|
||||||
|
MockPopoutButtonComponent,
|
||||||
|
MockCurrentAccountComponent,
|
||||||
|
GeneratorComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MockGeneratorPageComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-send-page",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-add-button></mock-add-button>
|
||||||
|
<mock-popout-button></mock-popout-button>
|
||||||
|
<mock-current-account></mock-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<send-placeholder></send-placeholder>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
MockAddButtonComponent,
|
||||||
|
MockPopoutButtonComponent,
|
||||||
|
MockCurrentAccountComponent,
|
||||||
|
SendComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MockSendPageComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-settings-page",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test">
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-add-button></mock-add-button>
|
||||||
|
<mock-popout-button></mock-popout-button>
|
||||||
|
<mock-current-account></mock-current-account>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<settings-placeholder></settings-placeholder>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
MockAddButtonComponent,
|
||||||
|
MockPopoutButtonComponent,
|
||||||
|
MockCurrentAccountComponent,
|
||||||
|
SettingsComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class MockSettingsPageComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "mock-vault-subpage",
|
||||||
|
template: `
|
||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" pageTitle="Test" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<mock-popout-button></mock-popout-button>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
<vault-placeholder></vault-placeholder>
|
||||||
|
<popup-footer slot="footer">
|
||||||
|
<button bitButton buttonType="primary">Save</button>
|
||||||
|
<button bitButton buttonType="secondary">Cancel</button>
|
||||||
|
</popup-footer>
|
||||||
|
</popup-page>
|
||||||
|
`,
|
||||||
|
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<PopupPageComponent>;
|
||||||
|
|
||||||
|
export const PopupTabNavigation: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /* HTML */ `
|
||||||
|
<extension-container>
|
||||||
|
<popup-tab-navigation>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</popup-tab-navigation>
|
||||||
|
</extension-container>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopupPage: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /* HTML */ `
|
||||||
|
<extension-container>
|
||||||
|
<mock-vault-page></mock-vault-page>
|
||||||
|
</extension-container>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopupPageWithFooter: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /* HTML */ `
|
||||||
|
<extension-container>
|
||||||
|
<mock-vault-subpage></mock-vault-subpage>
|
||||||
|
</extension-container>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PoppedOut: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /* HTML */ `
|
||||||
|
<div class="tw-h-[640px] tw-w-[900px] tw-border tw-border-solid tw-border-secondary-300">
|
||||||
|
<mock-vault-page-popped></mock-vault-page-popped>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
<ng-content select="[slot=header]"></ng-content>
|
||||||
|
<main class="tw-bg-background-alt tw-p-3 tw-flex-1 tw-overflow-y-auto">
|
||||||
|
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<ng-content select="[slot=footer]"></ng-content>
|
|
@ -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 {}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<div class="tw-h-full tw-overflow-y-auto [&>*]:tw-h-full [&>*]:tw-overflow-y-auto">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<footer class="tw-bg-background tw-border-0 tw-border-t tw-border-secondary-300 tw-border-solid">
|
||||||
|
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||||
|
<div class="tw-flex tw-flex-1">
|
||||||
|
<a
|
||||||
|
*ngFor="let button of navButtons"
|
||||||
|
class="tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-w-1/4 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500"
|
||||||
|
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
||||||
|
[title]="button.label"
|
||||||
|
[routerLink]="button.page"
|
||||||
|
routerLinkActive
|
||||||
|
#rla="routerLinkActive"
|
||||||
|
ariaCurrentWhenActive="page"
|
||||||
|
>
|
||||||
|
<i *ngIf="!rla.isActive" class="bwi bwi-lg bwi-{{ button.iconKey }}" aria-hidden="true"></i>
|
||||||
|
<i
|
||||||
|
*ngIf="rla.isActive"
|
||||||
|
class="bwi bwi-lg bwi-{{ button.iconKeyActive }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span
|
||||||
|
class="tw-truncate tw-max-w-full"
|
||||||
|
[ngClass]="!rla.isActive && 'group-hover:tw-underline'"
|
||||||
|
>
|
||||||
|
{{ button.label }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
|
@ -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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
@ -50,7 +49,6 @@ describe("Browser State Service", () => {
|
||||||
state.accounts[userId] = new Account({
|
state.accounts[userId] = new Account({
|
||||||
profile: { userId: userId },
|
profile: { userId: userId },
|
||||||
});
|
});
|
||||||
state.activeUserId = userId;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -78,18 +76,8 @@ describe("Browser State Service", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("add Account", () => {
|
it("exists", () => {
|
||||||
it("should add account", async () => {
|
expect(sut).toBeDefined();
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,8 +29,6 @@ export class DefaultBrowserStateService
|
||||||
initializeAs: "record",
|
initializeAs: "record",
|
||||||
})
|
})
|
||||||
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
||||||
@sessionSync({ initializer: (s: string) => s })
|
|
||||||
protected activeAccountSubject: BehaviorSubject<string>;
|
|
||||||
|
|
||||||
protected accountDeserializer = Account.fromJSON;
|
protected accountDeserializer = Account.fromJSON;
|
||||||
|
|
||||||
|
|
|
@ -200,26 +200,29 @@ export class LocalBackedSessionStorageService
|
||||||
}
|
}
|
||||||
|
|
||||||
private compareValues<T>(value1: T, value2: T): boolean {
|
private compareValues<T>(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;
|
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplicationVersion(): Promise<string> {
|
getApplicationVersion(): Promise<string> {
|
||||||
return Promise.resolve(BrowserApi.getApplicationVersion());
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
return Promise.resolve(manifest.version_name ?? manifest.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplicationVersionNumber(): Promise<string> {
|
getApplicationVersionNumber(): Promise<string> {
|
||||||
return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
|
const manifest = chrome.runtime.getManifest();
|
||||||
|
return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsWebAuthn(win: Window): boolean {
|
supportsWebAuthn(win: Window): boolean {
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||||
|
@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
|
||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
const [cacheKey, storageService] = storageLocation;
|
|
||||||
return new BackgroundDerivedState(
|
return new BackgroundDerivedState(
|
||||||
parentState$,
|
parentState$,
|
||||||
deriveDefinition,
|
deriveDefinition,
|
||||||
storageService,
|
deriveDefinition.buildCacheKey(),
|
||||||
cacheKey,
|
|
||||||
dependencies,
|
dependencies,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { Observable, Subscription } from "rxjs";
|
import { Observable, Subscription, concatMap } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import {
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
|
||||||
|
@ -22,11 +19,10 @@ export class BackgroundDerivedState<
|
||||||
constructor(
|
constructor(
|
||||||
parentState$: Observable<TFrom>,
|
parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
memoryStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
portName: string,
|
portName: string,
|
||||||
dependencies: TDeps,
|
dependencies: TDeps,
|
||||||
) {
|
) {
|
||||||
super(parentState$, deriveDefinition, memoryStorage, dependencies);
|
super(parentState$, deriveDefinition, dependencies);
|
||||||
|
|
||||||
// listen for foreground derived states to connect
|
// listen for foreground derived states to connect
|
||||||
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||||
|
@ -42,7 +38,20 @@ export class BackgroundDerivedState<
|
||||||
});
|
});
|
||||||
port.onMessage.addListener(listenerCallback);
|
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);
|
this.portSubscriptions.set(port, stateSubscription);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NgZone } from "@angular/core";
|
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 { mock } from "jest-mock-extended";
|
||||||
import { Subject, firstValueFrom } from "rxjs";
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-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";
|
import { mockPorts } from "../../../spec/mock-port.spec-util";
|
||||||
|
|
||||||
|
@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
|
||||||
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
||||||
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||||
deserializer: (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
|
// 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<Date>;
|
let foreground: ForegroundDerivedState<Date>;
|
||||||
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
||||||
let parentState$: Subject<string>;
|
let parentState$: Subject<string>;
|
||||||
let memoryStorage: FakeStorageService;
|
|
||||||
const initialParent = "2020-01-01";
|
const initialParent = "2020-01-01";
|
||||||
const ngZone = mock<NgZone>();
|
const ngZone = mock<NgZone>();
|
||||||
const portName = "testPort";
|
const portName = "testPort";
|
||||||
|
@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockPorts();
|
mockPorts();
|
||||||
parentState$ = new Subject<string>();
|
parentState$ = new Subject<string>();
|
||||||
memoryStorage = new FakeStorageService();
|
|
||||||
|
|
||||||
background = new BackgroundDerivedState(
|
background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
|
||||||
parentState$,
|
foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
deriveDefinition,
|
|
||||||
memoryStorage,
|
|
||||||
portName,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should initialize a late-connected foreground", async () => {
|
it("should initialize a late-connected foreground", async () => {
|
||||||
const newForeground = new ForegroundDerivedState(
|
const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
deriveDefinition,
|
const backgroundTracker = new ObservableTracker(background.state$);
|
||||||
memoryStorage,
|
|
||||||
portName,
|
|
||||||
ngZone,
|
|
||||||
);
|
|
||||||
const backgroundEmissions = trackEmissions(background.state$);
|
|
||||||
parentState$.next(initialParent);
|
parentState$.next(initialParent);
|
||||||
await awaitAsync();
|
const foregroundTracker = new ObservableTracker(newForeground.state$);
|
||||||
|
|
||||||
const foregroundEmissions = trackEmissions(newForeground.state$);
|
expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||||
await awaitAsync(10);
|
expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||||
|
|
||||||
expect(backgroundEmissions).toEqual([new Date(initialParent)]);
|
|
||||||
expect(foregroundEmissions).toEqual([new Date(initialParent)]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("forceValue", () => {
|
describe("forceValue", () => {
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { NgZone } from "@angular/core";
|
import { NgZone } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
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";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
// 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";
|
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";
|
import { ForegroundDerivedState } from "./foreground-derived-state";
|
||||||
|
|
||||||
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
||||||
constructor(
|
constructor(private ngZone: NgZone) {
|
||||||
storageServiceProvider: StorageServiceProvider,
|
super();
|
||||||
private ngZone: NgZone,
|
|
||||||
) {
|
|
||||||
super(storageServiceProvider);
|
|
||||||
}
|
}
|
||||||
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||||
_parentState$: Observable<TFrom>,
|
_parentState$: Observable<TFrom>,
|
||||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||||
_dependencies: TDeps,
|
_dependencies: TDeps,
|
||||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
|
||||||
): DerivedState<TTo> {
|
): DerivedState<TTo> {
|
||||||
const [cacheKey, storageService] = storageLocation;
|
return new ForegroundDerivedState(
|
||||||
return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone);
|
deriveDefinition,
|
||||||
|
deriveDefinition.buildCacheKey(),
|
||||||
|
this.ngZone,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { NgZone } from "@angular/core";
|
||||||
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
|
import { awaitAsync } from "@bitwarden/common/../spec";
|
||||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||||
|
@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
|
||||||
|
|
||||||
describe("ForegroundDerivedState", () => {
|
describe("ForegroundDerivedState", () => {
|
||||||
let sut: ForegroundDerivedState<Date>;
|
let sut: ForegroundDerivedState<Date>;
|
||||||
let memoryStorage: FakeStorageService;
|
|
||||||
const portName = "testPort";
|
const portName = "testPort";
|
||||||
const ngZone = mock<NgZone>();
|
const ngZone = mock<NgZone>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
memoryStorage = new FakeStorageService();
|
|
||||||
memoryStorage.internalUpdateValuesRequireDeserialization(true);
|
|
||||||
mockPorts();
|
mockPorts();
|
||||||
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
|
||||||
expect(disconnectSpy).toHaveBeenCalled();
|
expect(disconnectSpy).toHaveBeenCalled();
|
||||||
expect(sut["port"]).toBeNull();
|
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)]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,19 +6,14 @@ import {
|
||||||
filter,
|
filter,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
merge,
|
|
||||||
of,
|
of,
|
||||||
share,
|
share,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
tap,
|
||||||
timer,
|
timer,
|
||||||
} from "rxjs";
|
} 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||||
import { DerivedStateDependencies } from "@bitwarden/common/types/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";
|
import { runInsideAngular } from "../browser/run-inside-angular.operator";
|
||||||
|
|
||||||
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||||
private storageKey: string;
|
|
||||||
private port: chrome.runtime.Port;
|
private port: chrome.runtime.Port;
|
||||||
private backgroundResponses$: Observable<DerivedStateMessage>;
|
private backgroundResponses$: Observable<DerivedStateMessage>;
|
||||||
state$: Observable<TTo>;
|
state$: Observable<TTo>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
||||||
private memoryStorage: AbstractStorageService & ObservableStorageService,
|
|
||||||
private portName: string,
|
private portName: string,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
) {
|
) {
|
||||||
this.storageKey = deriveDefinition.storageKey;
|
const latestValueFromPort$ = (port: chrome.runtime.Port) => {
|
||||||
|
return fromChromeEvent(port.onMessage).pipe(
|
||||||
const initialStorageGet$ = defer(() => {
|
map(([message]) => message as DerivedStateMessage),
|
||||||
return this.getStoredValue();
|
filter((message) => message.originator === "background" && message.action === "nextState"),
|
||||||
}).pipe(
|
map((message) => {
|
||||||
filter((s) => s.derived),
|
const json = JSON.parse(message.data) as Jsonify<TTo>;
|
||||||
map((s) => s.value),
|
return this.deriveDefinition.deserialize(json);
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.state$ = defer(() => of(this.initializePort())).pipe(
|
this.state$ = defer(() => of(this.initializePort())).pipe(
|
||||||
switchMap(() => merge(initialStorageGet$, latestStorage$)),
|
switchMap(() => latestValueFromPort$(this.port)),
|
||||||
share({
|
share({
|
||||||
connector: () => new ReplaySubject<TTo>(1),
|
connector: () => new ReplaySubject<TTo>(1),
|
||||||
resetOnRefCountZero: () =>
|
resetOnRefCountZero: () =>
|
||||||
|
@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||||
this.port = null;
|
this.port = null;
|
||||||
this.backgroundResponses$ = 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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@ -27,8 +29,9 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||||
</div>`,
|
</div>`,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private lastActivity: number = null;
|
private lastActivity: Date;
|
||||||
private activeUserId: string;
|
private activeUserId: UserId;
|
||||||
|
private recordActivitySubject = new Subject<void>();
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@ -46,6 +49,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private messageListener: MessageListener,
|
private messageListener: MessageListener,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -53,14 +57,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
// Clear them aggressively to make sure this doesn't occur
|
// Clear them aggressively to make sure this doesn't occur
|
||||||
await this.clearComponentStates();
|
await this.clearComponentStates();
|
||||||
|
|
||||||
this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
|
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||||
this.activeUserId = userId;
|
this.activeUserId = account?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.authService.activeAccountStatus$
|
this.authService.activeAccountStatus$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((status) => status === AuthenticationStatus.Unlocked),
|
filter((status) => status === AuthenticationStatus.Unlocked),
|
||||||
filter((unlocked) => unlocked),
|
|
||||||
concatMap(async () => {
|
concatMap(async () => {
|
||||||
await this.recordActivity();
|
await this.recordActivity();
|
||||||
}),
|
}),
|
||||||
|
@ -200,13 +203,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().getTime();
|
const now = new Date();
|
||||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastActivity = now;
|
this.lastActivity = now;
|
||||||
await this.stateService.setLastActive(now, { userId: this.activeUserId });
|
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showToast(msg: any) {
|
private showToast(msg: any) {
|
||||||
|
|
|
@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
import { HeaderComponent } from "../platform/popup/header.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 { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||||
|
@ -108,6 +112,10 @@ import "../platform/popup/locales";
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupTabNavigationComponent,
|
||||||
|
PopupFooterComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ActionButtonsComponent,
|
ActionButtonsComponent,
|
||||||
|
|
|
@ -68,7 +68,7 @@ img {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a:not(popup-page a, popup-tab-navigation a) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
@include themify($themes) {
|
@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;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -448,7 +448,7 @@ app-root {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main:not(popup-page main) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 44px;
|
top: 44px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
SYSTEM_THEME_OBSERVABLE,
|
SYSTEM_THEME_OBSERVABLE,
|
||||||
SafeInjectionToken,
|
SafeInjectionToken,
|
||||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
@ -45,6 +46,7 @@ import {
|
||||||
UserNotificationSettingsServiceAbstraction,
|
UserNotificationSettingsServiceAbstraction,
|
||||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
@ -473,7 +475,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DerivedStateProvider,
|
provide: DerivedStateProvider,
|
||||||
useClass: ForegroundDerivedStateProvider,
|
useClass: ForegroundDerivedStateProvider,
|
||||||
deps: [StorageServiceProvider, NgZone],
|
deps: [NgZone],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AutofillSettingsServiceAbstraction,
|
provide: AutofillSettingsServiceAbstraction,
|
||||||
|
@ -558,6 +560,10 @@ const safeProviders: SafeProvider[] = [
|
||||||
OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
|
OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CLIENT_TYPE,
|
||||||
|
useValue: ClientType.Browser,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div bitDialogTitle>Bitwarden</div>
|
<div bitDialogTitle>Bitwarden</div>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||||
<p>{{ "version" | i18n }}: {{ version }}</p>
|
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<p *ngIf="data.isCloud">
|
<p *ngIf="data.isCloud">
|
||||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "about.component.html",
|
templateUrl: "about.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutComponent {
|
||||||
protected year = new Date().getFullYear();
|
protected year = new Date().getFullYear();
|
||||||
protected version = BrowserApi.getApplicationVersion();
|
protected version$: Observable<string>;
|
||||||
|
|
||||||
protected data$ = combineLatest([
|
protected data$ = combineLatest([
|
||||||
this.configService.serverConfig$,
|
this.configService.serverConfig$,
|
||||||
|
@ -26,5 +25,8 @@ export class AboutComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
) {}
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {
|
||||||
|
this.version$ = defer(() => this.platformUtilsService.getApplicationVersion());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
|
@ -86,6 +87,7 @@ export class SettingsComponent implements OnInit {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@ -434,8 +436,9 @@ export class SettingsComponent implements OnInit {
|
||||||
type: "info",
|
type: "info",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.messagingService.send("logout");
|
this.messagingService.send("logout", { userId: userId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag<boolean>(
|
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
|
||||||
FeatureFlag.BrowserFilelessImport,
|
FeatureFlag.BrowserFilelessImport,
|
||||||
);
|
);
|
||||||
const userAuthStatus = await this.authService.getAuthStatus();
|
const userAuthStatus = await this.authService.getAuthStatus();
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { first } from "rxjs/operators";
|
||||||
|
|
||||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
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 { 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -51,6 +52,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||||
formBuilder: FormBuilder,
|
formBuilder: FormBuilder,
|
||||||
private filePopoutUtilsService: FilePopoutUtilsService,
|
private filePopoutUtilsService: FilePopoutUtilsService,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -66,6 +68,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||||
dialogService,
|
dialogService,
|
||||||
formBuilder,
|
formBuilder,
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"tldts": "6.1.16",
|
"tldts": "6.1.18",
|
||||||
"zxcvbn": "4.4.2"
|
"zxcvbn": "4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as path from "path";
|
||||||
|
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import * as jsdom from "jsdom";
|
import * as jsdom from "jsdom";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
|
@ -79,7 +80,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
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 {
|
import {
|
||||||
ActiveUserStateProvider,
|
ActiveUserStateProvider,
|
||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
|
@ -236,7 +237,7 @@ export class Main {
|
||||||
biometricStateService: BiometricStateService;
|
biometricStateService: BiometricStateService;
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||||
providerApiService: ProviderApiServiceAbstraction;
|
providerApiService: ProviderApiServiceAbstraction;
|
||||||
userKeyInitService: UserKeyInitService;
|
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||||
kdfConfigService: KdfConfigServiceAbstraction;
|
kdfConfigService: KdfConfigServiceAbstraction;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -314,7 +315,7 @@ export class Main {
|
||||||
this.singleUserStateProvider,
|
this.singleUserStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
|
this.derivedStateProvider = new DefaultDerivedStateProvider();
|
||||||
|
|
||||||
this.stateProvider = new DefaultStateProvider(
|
this.stateProvider = new DefaultStateProvider(
|
||||||
this.activeUserStateProvider,
|
this.activeUserStateProvider,
|
||||||
|
@ -344,6 +345,7 @@ export class Main {
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
new MigrationBuilderService(),
|
new MigrationBuilderService(),
|
||||||
|
ClientType.Cli,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.stateService = new StateService(
|
this.stateService = new StateService(
|
||||||
|
@ -708,11 +710,7 @@ export class Main {
|
||||||
|
|
||||||
this.providerApiService = new ProviderApiService(this.apiService);
|
this.providerApiService = new ProviderApiService(this.apiService);
|
||||||
|
|
||||||
this.userKeyInitService = new UserKeyInitService(
|
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||||
this.accountService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.logService,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -733,7 +731,7 @@ export class Main {
|
||||||
this.authService.logOut(() => {
|
this.authService.logOut(() => {
|
||||||
/* Do nothing */
|
/* Do nothing */
|
||||||
});
|
});
|
||||||
const userId = await this.stateService.getUserId();
|
const userId = (await this.stateService.getUserId()) as UserId;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.eventUploadService.uploadEvents(userId as UserId),
|
this.eventUploadService.uploadEvents(userId as UserId),
|
||||||
this.syncService.setLastSync(new Date(0)),
|
this.syncService.setLastSync(new Date(0)),
|
||||||
|
@ -744,9 +742,10 @@ export class Main {
|
||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||||
|
|
||||||
await this.stateService.clean();
|
await this.stateService.clean();
|
||||||
|
await this.accountService.clean(userId);
|
||||||
process.env.BW_SESSION = null;
|
process.env.BW_SESSION = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -756,7 +755,11 @@ export class Main {
|
||||||
this.containerService.attachToGlobal(global);
|
this.containerService.attachToGlobal(global);
|
||||||
await this.i18nService.init();
|
await this.i18nService.init();
|
||||||
this.twoFactorService.init();
|
this.twoFactorService.init();
|
||||||
this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
|
|
||||||
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
if (activeAccount) {
|
||||||
|
await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec";
|
||||||
|
|
||||||
import { ConsoleLogService } from "./console-log.service";
|
import { ConsoleLogService } from "./console-log.service";
|
||||||
|
|
||||||
let caughtMessage: any = {};
|
|
||||||
|
|
||||||
describe("CLI Console log service", () => {
|
describe("CLI Console log service", () => {
|
||||||
|
const error = new Error("this is an error");
|
||||||
|
const obj = { a: 1, b: 2 };
|
||||||
let logService: ConsoleLogService;
|
let logService: ConsoleLogService;
|
||||||
|
let consoleSpy: {
|
||||||
|
log: jest.Mock<any, any>;
|
||||||
|
warn: jest.Mock<any, any>;
|
||||||
|
error: jest.Mock<any, any>;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
caughtMessage = {};
|
consoleSpy = interceptConsole();
|
||||||
interceptConsole(caughtMessage);
|
|
||||||
logService = new ConsoleLogService(true);
|
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", () => {
|
it("should redirect all console to error if BW_RESPONSE env is true", () => {
|
||||||
process.env.BW_RESPONSE = "true";
|
process.env.BW_RESPONSE = "true";
|
||||||
|
|
||||||
logService.debug("this is a debug message");
|
logService.debug("this is a debug message", error, obj);
|
||||||
expect(caughtMessage).toMatchObject({
|
expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj);
|
||||||
error: { 0: "this is a debug message" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not redirect console to error if BW_RESPONSE != true", () => {
|
it("should not redirect console to error if BW_RESPONSE != true", () => {
|
||||||
process.env.BW_RESPONSE = "false";
|
process.env.BW_RESPONSE = "false";
|
||||||
|
|
||||||
logService.debug("debug");
|
logService.debug("debug", error, obj);
|
||||||
logService.info("info");
|
logService.info("info", error, obj);
|
||||||
logService.warning("warning");
|
logService.warning("warning", error, obj);
|
||||||
logService.error("error");
|
logService.error("error", error, obj);
|
||||||
|
|
||||||
expect(caughtMessage).toMatchObject({
|
expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj);
|
||||||
log: { 0: "info" },
|
expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj);
|
||||||
warn: { 0: "warning" },
|
expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj);
|
||||||
error: { 0: "error" },
|
expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService {
|
||||||
super(isDev, filter);
|
super(isDev, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
write(level: LogLevelType, message: string) {
|
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
|
||||||
if (this.filter != null && this.filter(level)) {
|
if (this.filter != null && this.filter(level)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.BW_RESPONSE === "true") {
|
if (process.env.BW_RESPONSE === "true") {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.error(message);
|
console.error(message, ...optionalParams);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.write(level, message);
|
super.write(level, message, ...optionalParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} from "@bitwarden/angular/auth/guards";
|
||||||
|
|
||||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
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 { HintComponent } from "../auth/hint.component";
|
||||||
import { LockComponent } from "../auth/lock.component";
|
import { LockComponent } from "../auth/lock.component";
|
||||||
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
||||||
|
@ -40,7 +40,7 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "login",
|
||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
canActivate: [LoginGuard],
|
canActivate: [maxAccountsGuardFn()],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "login-with-device",
|
path: "login-with-device",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
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 { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.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 { 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 { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
@ -107,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
private lastActivity: number = null;
|
private lastActivity: Date = null;
|
||||||
private modal: ModalRef = null;
|
private modal: ModalRef = null;
|
||||||
private idleTimer: number = null;
|
private idleTimer: number = null;
|
||||||
private isIdle = false;
|
private isIdle = false;
|
||||||
private activeUserId: string = null;
|
private activeUserId: UserId = null;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
@ -150,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private stateEventRunnerService: StateEventRunnerService,
|
private stateEventRunnerService: StateEventRunnerService,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
private organizationService: InternalOrganizationServiceAbstraction,
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
|
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||||
this.activeUserId = userId;
|
this.activeUserId = account?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ngZone.runOutsideAngular(() => {
|
this.ngZone.runOutsideAngular(() => {
|
||||||
|
@ -400,7 +400,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
break;
|
break;
|
||||||
case "switchAccount": {
|
case "switchAccount": {
|
||||||
if (message.userId != null) {
|
if (message.userId != null) {
|
||||||
await this.stateService.setActiveUser(message.userId);
|
await this.stateService.clearDecryptedData(message.userId);
|
||||||
|
await this.accountService.switchAccount(message.userId);
|
||||||
}
|
}
|
||||||
const locked =
|
const locked =
|
||||||
(await this.authService.getAuthStatus(message.userId)) ===
|
(await this.authService.getAuthStatus(message.userId)) ===
|
||||||
|
@ -522,7 +523,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private async updateAppMenu() {
|
private async updateAppMenu() {
|
||||||
let updateRequest: MenuUpdateRequest;
|
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) {
|
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
|
||||||
updateRequest = {
|
updateRequest = {
|
||||||
accounts: null,
|
accounts: null,
|
||||||
|
@ -531,32 +532,32 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
} else {
|
} else {
|
||||||
const accounts: { [userId: string]: MenuAccount } = {};
|
const accounts: { [userId: string]: MenuAccount } = {};
|
||||||
for (const i in stateAccounts) {
|
for (const i in stateAccounts) {
|
||||||
|
const userId = i as UserId;
|
||||||
if (
|
if (
|
||||||
i != null &&
|
i != null &&
|
||||||
stateAccounts[i]?.profile?.userId != null &&
|
userId != null &&
|
||||||
!this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up
|
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
|
||||||
) {
|
) {
|
||||||
const userId = stateAccounts[i].profile.userId;
|
|
||||||
const availableTimeoutActions = await firstValueFrom(
|
const availableTimeoutActions = await firstValueFrom(
|
||||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||||
accounts[userId] = {
|
accounts[userId] = {
|
||||||
isAuthenticated: await this.stateService.getIsAuthenticated({
|
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
|
||||||
userId: userId,
|
isLocked: authStatus === AuthenticationStatus.Locked,
|
||||||
}),
|
|
||||||
isLocked:
|
|
||||||
(await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
|
|
||||||
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
|
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
|
||||||
email: stateAccounts[i].profile.email,
|
email: stateAccounts[userId].email,
|
||||||
userId: stateAccounts[i].profile.userId,
|
userId: userId,
|
||||||
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
|
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateRequest = {
|
updateRequest = {
|
||||||
accounts: accounts,
|
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) {
|
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)
|
// 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
|
// 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);
|
this.startAccountCleanUp(userBeingLoggedOut);
|
||||||
|
|
||||||
let preLogoutActiveUserId;
|
let preLogoutActiveUserId;
|
||||||
|
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||||
try {
|
try {
|
||||||
// Provide the userId of the user to upload events for
|
// 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.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||||
await this.cipherService.clear(userBeingLoggedOut);
|
await this.cipherService.clear(userBeingLoggedOut);
|
||||||
|
@ -582,22 +586,23 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
await this.collectionService.clear(userBeingLoggedOut);
|
await this.collectionService.clear(userBeingLoggedOut);
|
||||||
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
||||||
await this.vaultTimeoutSettingsService.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;
|
preLogoutActiveUserId = this.activeUserId;
|
||||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||||
|
await this.accountService.clean(userBeingLoggedOut);
|
||||||
} finally {
|
} finally {
|
||||||
this.finishAccountCleanUp(userBeingLoggedOut);
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate(["login"]);
|
this.router.navigate(["login"]);
|
||||||
} else if (preLogoutActiveUserId !== this.activeUserId) {
|
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
|
||||||
this.messagingService.send("switchAccount");
|
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateAppMenu();
|
await this.updateAppMenu();
|
||||||
|
@ -622,13 +627,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().getTime();
|
const now = new Date();
|
||||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastActivity = now;
|
this.lastActivity = now;
|
||||||
await this.stateService.setLastActive(now, { userId: this.activeUserId });
|
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||||
|
|
||||||
// Idle states
|
// Idle states
|
||||||
if (this.isIdle) {
|
if (this.isIdle) {
|
||||||
|
|
|
@ -1,110 +1,112 @@
|
||||||
<!-- Please remove this disable statement when editing this file! -->
|
<!-- Please remove this disable statement when editing this file! -->
|
||||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||||
<button
|
<ng-container *ngIf="view$ | async as view">
|
||||||
class="account-switcher"
|
<button
|
||||||
(click)="toggle()"
|
class="account-switcher"
|
||||||
cdkOverlayOrigin
|
(click)="toggle()"
|
||||||
#trigger="cdkOverlayOrigin"
|
cdkOverlayOrigin
|
||||||
[hidden]="!showSwitcher"
|
#trigger="cdkOverlayOrigin"
|
||||||
aria-haspopup="dialog"
|
[hidden]="!view.showSwitcher"
|
||||||
>
|
aria-haspopup="dialog"
|
||||||
<ng-container *ngIf="activeAccount?.email != null; else noActiveAccount">
|
|
||||||
<app-avatar
|
|
||||||
[text]="activeAccount.name"
|
|
||||||
[id]="activeAccount.id"
|
|
||||||
[color]="activeAccount.avatarColor"
|
|
||||||
[size]="25"
|
|
||||||
[circle]="true"
|
|
||||||
[fontSize]="14"
|
|
||||||
[dynamic]="true"
|
|
||||||
*ngIf="activeAccount.email != null"
|
|
||||||
aria-hidden="true"
|
|
||||||
></app-avatar>
|
|
||||||
<div class="active-account">
|
|
||||||
<div>{{ activeAccount.email }}</div>
|
|
||||||
<span>{{ activeAccount.server }}</span>
|
|
||||||
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template #noActiveAccount>
|
|
||||||
<span>{{ "switchAccount" | i18n }}</span>
|
|
||||||
</ng-template>
|
|
||||||
<i
|
|
||||||
class="bwi"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ng-template
|
|
||||||
cdkConnectedOverlay
|
|
||||||
[cdkConnectedOverlayOrigin]="trigger"
|
|
||||||
[cdkConnectedOverlayHasBackdrop]="true"
|
|
||||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
|
||||||
(backdropClick)="close()"
|
|
||||||
(detach)="close()"
|
|
||||||
[cdkConnectedOverlayOpen]="showSwitcher && isOpen"
|
|
||||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
|
||||||
cdkConnectedOverlayMinWidth="250px"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="account-switcher-dropdown"
|
|
||||||
[@transformPanel]="'open'"
|
|
||||||
cdkTrapFocus
|
|
||||||
cdkTrapFocusAutoCapture
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
>
|
||||||
<div class="accounts" *ngIf="numberOfAccounts > 0">
|
<ng-container *ngIf="view.activeAccount; else noActiveAccount">
|
||||||
<button
|
<app-avatar
|
||||||
*ngFor="let account of inactiveAccounts | keyvalue"
|
[text]="view.activeAccount.name ?? view.activeAccount.email"
|
||||||
class="account"
|
[id]="view.activeAccount.id"
|
||||||
(click)="switch(account.key)"
|
[color]="view.activeAccount.avatarColor"
|
||||||
>
|
[size]="25"
|
||||||
<app-avatar
|
[circle]="true"
|
||||||
[text]="account.value.name ?? account.value.email"
|
[fontSize]="14"
|
||||||
[id]="account.value.id"
|
[dynamic]="true"
|
||||||
[size]="25"
|
*ngIf="view.activeAccount.email != null"
|
||||||
[circle]="true"
|
aria-hidden="true"
|
||||||
[fontSize]="14"
|
></app-avatar>
|
||||||
[dynamic]="true"
|
<div class="active-account">
|
||||||
[color]="account.value.avatarColor"
|
<div>{{ view.activeAccount.email }}</div>
|
||||||
*ngIf="account.value.email != null"
|
<span>{{ view.activeAccount.server }}</span>
|
||||||
aria-hidden="true"
|
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
||||||
></app-avatar>
|
</div>
|
||||||
<div class="accountInfo">
|
|
||||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
|
||||||
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
|
||||||
<span class="server" aria-hidden="true">
|
|
||||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
|
||||||
</span>
|
|
||||||
<span class="status" aria-hidden="true"
|
|
||||||
><span class="sr-only"> (</span
|
|
||||||
>{{
|
|
||||||
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
|
||||||
| i18n
|
|
||||||
}}<span class="sr-only">)</span></span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-2x text-muted"
|
|
||||||
[ngClass]="
|
|
||||||
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="activeAccount?.email != null">
|
|
||||||
<div class="border" *ngIf="numberOfAccounts > 0"></div>
|
|
||||||
<ng-container *ngIf="numberOfAccounts < 4">
|
|
||||||
<button type="button" class="add" (click)="addAccount()">
|
|
||||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="numberOfAccounts === 4">
|
|
||||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
<ng-template #noActiveAccount>
|
||||||
</ng-template>
|
<span>{{ "switchAccount" | i18n }}</span>
|
||||||
|
</ng-template>
|
||||||
|
<i
|
||||||
|
class="bwi"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-template
|
||||||
|
cdkConnectedOverlay
|
||||||
|
[cdkConnectedOverlayOrigin]="trigger"
|
||||||
|
[cdkConnectedOverlayHasBackdrop]="true"
|
||||||
|
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||||
|
(backdropClick)="close()"
|
||||||
|
(detach)="close()"
|
||||||
|
[cdkConnectedOverlayOpen]="view.showSwitcher && isOpen"
|
||||||
|
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||||
|
cdkConnectedOverlayMinWidth="250px"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="account-switcher-dropdown"
|
||||||
|
[@transformPanel]="'open'"
|
||||||
|
cdkTrapFocus
|
||||||
|
cdkTrapFocusAutoCapture
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<div class="accounts" *ngIf="view.numberOfAccounts > 0">
|
||||||
|
<button
|
||||||
|
*ngFor="let account of view.inactiveAccounts | keyvalue"
|
||||||
|
class="account"
|
||||||
|
(click)="switch(account.key)"
|
||||||
|
>
|
||||||
|
<app-avatar
|
||||||
|
[text]="account.value.name ?? account.value.email"
|
||||||
|
[id]="account.value.id"
|
||||||
|
[size]="25"
|
||||||
|
[circle]="true"
|
||||||
|
[fontSize]="14"
|
||||||
|
[dynamic]="true"
|
||||||
|
[color]="account.value.avatarColor"
|
||||||
|
*ngIf="account.value.email != null"
|
||||||
|
aria-hidden="true"
|
||||||
|
></app-avatar>
|
||||||
|
<div class="accountInfo">
|
||||||
|
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||||
|
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||||
|
<span class="server" aria-hidden="true">
|
||||||
|
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||||
|
</span>
|
||||||
|
<span class="status" aria-hidden="true"
|
||||||
|
><span class="sr-only"> (</span
|
||||||
|
>{{
|
||||||
|
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||||
|
| i18n
|
||||||
|
}}<span class="sr-only">)</span></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-2x text-muted"
|
||||||
|
[ngClass]="
|
||||||
|
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||||
|
"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="view.activeAccount">
|
||||||
|
<div class="border" *ngIf="view.numberOfAccounts > 0"></div>
|
||||||
|
<ng-container *ngIf="view.numberOfAccounts < 4">
|
||||||
|
<button type="button" class="add" (click)="addAccount()">
|
||||||
|
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="view.numberOfAccounts === 4">
|
||||||
|
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.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";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
type ActiveAccount = {
|
type ActiveAccount = {
|
||||||
|
@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & {
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
export class AccountSwitcherComponent {
|
||||||
activeAccount?: ActiveAccount;
|
activeAccount$: Observable<ActiveAccount | null>;
|
||||||
inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
||||||
|
|
||||||
authStatus = AuthenticationStatus;
|
authStatus = AuthenticationStatus;
|
||||||
|
|
||||||
|
view$: Observable<{
|
||||||
|
activeAccount: ActiveAccount | null;
|
||||||
|
inactiveAccounts: { [userId: string]: InactiveAccount };
|
||||||
|
numberOfAccounts: number;
|
||||||
|
showSwitcher: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
overlayPosition: ConnectedPosition[] = [
|
overlayPosition: ConnectedPosition[] = [
|
||||||
{
|
{
|
||||||
|
@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
showSwitcher$: Observable<boolean>;
|
||||||
|
|
||||||
get showSwitcher() {
|
numberOfAccounts$: Observable<number>;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
private avatarService: AvatarService,
|
private avatarService: AvatarService,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private tokenService: TokenService,
|
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private loginEmailService: LoginEmailServiceAbstraction,
|
private loginEmailService: LoginEmailServiceAbstraction,
|
||||||
) {}
|
private accountService: AccountService,
|
||||||
|
) {
|
||||||
|
this.activeAccount$ = this.accountService.activeAccount$.pipe(
|
||||||
|
switchMap(async (active) => {
|
||||||
|
if (active == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
return {
|
||||||
this.stateService.accounts$
|
id: active.id,
|
||||||
.pipe(
|
name: active.name,
|
||||||
concatMap(async (accounts: { [userId: string]: Account }) => {
|
email: active.email,
|
||||||
this.inactiveAccounts = await this.createInactiveAccounts(accounts);
|
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.view$ = combineLatest([
|
||||||
this.activeAccount = {
|
this.activeAccount$,
|
||||||
id: await this.tokenService.getUserId(),
|
this.inactiveAccounts$,
|
||||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
this.numberOfAccounts$,
|
||||||
email: await this.tokenService.getEmail(),
|
this.showSwitcher$,
|
||||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
]).pipe(
|
||||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({
|
||||||
};
|
activeAccount,
|
||||||
} catch {
|
inactiveAccounts,
|
||||||
this.activeAccount = undefined;
|
numberOfAccounts,
|
||||||
}
|
showSwitcher,
|
||||||
}),
|
})),
|
||||||
takeUntil(this.destroy$),
|
);
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
|
@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
await this.loginEmailService.saveEmailSettings();
|
await this.loginEmailService.saveEmailSettings();
|
||||||
|
|
||||||
await this.router.navigate(["/login"]);
|
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: {
|
private async createInactiveAccounts(baseAccounts: {
|
||||||
[userId: string]: Account;
|
[userId: string]: AccountInfo;
|
||||||
}): Promise<{ [userId: string]: InactiveAccount }> {
|
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||||
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||||
|
|
||||||
|
@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
inactiveAccounts[userId] = {
|
inactiveAccounts[userId] = {
|
||||||
id: userId,
|
id: userId,
|
||||||
name: baseAccounts[userId].profile.name,
|
name: baseAccounts[userId].name,
|
||||||
email: baseAccounts[userId].profile.email,
|
email: baseAccounts[userId].email,
|
||||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
||||||
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
|
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { UntypedFormControl } from "@angular/forms";
|
import { UntypedFormControl } from "@angular/forms";
|
||||||
import { Subscription } from "rxjs";
|
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";
|
import { SearchBarService, SearchBarState } from "./search-bar.service";
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private searchBarService: SearchBarService,
|
private searchBarService: SearchBarService,
|
||||||
private stateService: StateService,
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
this.searchBarService.state$.subscribe((state) => {
|
this.searchBarService.state$.subscribe((state) => {
|
||||||
|
@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// 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.searchBarService.setSearchText("");
|
||||||
this.searchText.patchValue("");
|
this.searchText.patchValue("");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { DOCUMENT } from "@angular/common";
|
import { DOCUMENT } from "@angular/common";
|
||||||
import { Inject, Injectable } from "@angular/core";
|
import { Inject, Injectable } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.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 { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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 { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.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 { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.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 { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
|
@ -36,7 +39,8 @@ export class InitService {
|
||||||
private nativeMessagingService: NativeMessagingService,
|
private nativeMessagingService: NativeMessagingService,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private userKeyInitService: UserKeyInitService,
|
private userAutoUnlockKeyService: UserAutoUnlockKeyService,
|
||||||
|
private accountService: AccountService,
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -44,7 +48,18 @@ export class InitService {
|
||||||
return async () => {
|
return async () => {
|
||||||
this.nativeMessagingService.init();
|
this.nativeMessagingService.init();
|
||||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
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.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
SafeInjectionToken,
|
SafeInjectionToken,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.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 { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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 { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { LoginGuard } from "../../auth/guards/login.guard";
|
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||||
|
@ -100,7 +101,6 @@ const safeProviders: SafeProvider[] = [
|
||||||
safeProvider(InitService),
|
safeProvider(InitService),
|
||||||
safeProvider(NativeMessagingService),
|
safeProvider(NativeMessagingService),
|
||||||
safeProvider(SearchBarService),
|
safeProvider(SearchBarService),
|
||||||
safeProvider(LoginGuard),
|
|
||||||
safeProvider(DialogService),
|
safeProvider(DialogService),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
||||||
|
@ -190,6 +190,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
AutofillSettingsServiceAbstraction,
|
AutofillSettingsServiceAbstraction,
|
||||||
VaultTimeoutSettingsService,
|
VaultTimeoutSettingsService,
|
||||||
BiometricStateService,
|
BiometricStateService,
|
||||||
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
@ -275,6 +276,10 @@ const safeProviders: SafeProvider[] = [
|
||||||
useClass: NativeMessagingManifestService,
|
useClass: NativeMessagingManifestService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CLIENT_TYPE,
|
||||||
|
useValue: ClientType.Desktop,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
@ -59,6 +60,10 @@ describe("GeneratorComponent", () => {
|
||||||
provide: CipherService,
|
provide: CipherService,
|
||||||
useValue: mock<CipherService>(),
|
useValue: mock<CipherService>(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AccountService,
|
||||||
|
useValue: mock<AccountService>(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
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 { 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
formBuilder: FormBuilder,
|
formBuilder: FormBuilder,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||||
dialogService,
|
dialogService,
|
||||||
formBuilder,
|
formBuilder,
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<boolean> {
|
||||||
|
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();
|
||||||
|
}
|
|
@ -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 { 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 { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
@ -50,7 +51,7 @@ describe("LockComponent", () => {
|
||||||
let component: LockComponent;
|
let component: LockComponent;
|
||||||
let fixture: ComponentFixture<LockComponent>;
|
let fixture: ComponentFixture<LockComponent>;
|
||||||
let stateServiceMock: MockProxy<StateService>;
|
let stateServiceMock: MockProxy<StateService>;
|
||||||
const biometricStateService = mock<BiometricStateService>();
|
let biometricStateService: MockProxy<BiometricStateService>;
|
||||||
let messagingServiceMock: MockProxy<MessagingService>;
|
let messagingServiceMock: MockProxy<MessagingService>;
|
||||||
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
||||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||||
|
@ -62,7 +63,6 @@ describe("LockComponent", () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
stateServiceMock = mock<StateService>();
|
stateServiceMock = mock<StateService>();
|
||||||
stateServiceMock.activeAccount$ = of(null);
|
|
||||||
|
|
||||||
messagingServiceMock = mock<MessagingService>();
|
messagingServiceMock = mock<MessagingService>();
|
||||||
broadcasterServiceMock = mock<BroadcasterService>();
|
broadcasterServiceMock = mock<BroadcasterService>();
|
||||||
|
@ -73,6 +73,7 @@ describe("LockComponent", () => {
|
||||||
|
|
||||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||||
|
|
||||||
|
biometricStateService = mock();
|
||||||
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
|
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
|
||||||
biometricStateService.promptAutomatically$ = of(false);
|
biometricStateService.promptAutomatically$ = of(false);
|
||||||
biometricStateService.promptCancelled$ = of(false);
|
biometricStateService.promptCancelled$ = of(false);
|
||||||
|
@ -165,6 +166,10 @@ describe("LockComponent", () => {
|
||||||
provide: AccountService,
|
provide: AccountService,
|
||||||
useValue: accountService,
|
useValue: accountService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: mock(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: KdfConfigService,
|
provide: KdfConfigService,
|
||||||
useValue: mock<KdfConfigService>(),
|
useValue: mock<KdfConfigService>(),
|
||||||
|
|
|
@ -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 { 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 { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
@ -64,6 +65,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinCryptoService: PinCryptoServiceAbstraction,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
authService: AuthService,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
|
@ -89,6 +91,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
pinCryptoService,
|
pinCryptoService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
accountService,
|
accountService,
|
||||||
|
authService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1636,7 +1636,7 @@
|
||||||
"message": "Error enabling browser integration"
|
"message": "Error enabling browser integration"
|
||||||
},
|
},
|
||||||
"browserIntegrationErrorDesc": {
|
"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": {
|
"browserIntegrationMasOnlyDesc": {
|
||||||
"message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment."
|
"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"
|
"description": "Label indicating the most common import formats"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "Succès"
|
||||||
},
|
},
|
||||||
"troubleshooting": {
|
"troubleshooting": {
|
||||||
"message": "Résolution de problèmes"
|
"message": "Résolution de problèmes"
|
||||||
|
|
|
@ -2698,7 +2698,7 @@
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "Sikeres"
|
||||||
},
|
},
|
||||||
"troubleshooting": {
|
"troubleshooting": {
|
||||||
"message": "Hibaelhárítás"
|
"message": "Hibaelhárítás"
|
||||||
|
|
|
@ -2698,7 +2698,7 @@
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "Izdevās"
|
||||||
},
|
},
|
||||||
"troubleshooting": {
|
"troubleshooting": {
|
||||||
"message": "Sarežģījumu novēršana"
|
"message": "Sarežģījumu novēršana"
|
||||||
|
|
|
@ -2698,7 +2698,7 @@
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "Succes"
|
||||||
},
|
},
|
||||||
"troubleshooting": {
|
"troubleshooting": {
|
||||||
"message": "Probleemoplossing"
|
"message": "Probleemoplossing"
|
||||||
|
|
|
@ -2698,7 +2698,7 @@
|
||||||
"description": "Label indicating the most common import formats"
|
"description": "Label indicating the most common import formats"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"message": "Success"
|
"message": "成功"
|
||||||
},
|
},
|
||||||
"troubleshooting": {
|
"troubleshooting": {
|
||||||
"message": "故障排除"
|
"message": "故障排除"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Subject, firstValueFrom } from "rxjs";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/services/token.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 { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
@ -157,7 +158,7 @@ export class Main {
|
||||||
activeUserStateProvider,
|
activeUserStateProvider,
|
||||||
singleUserStateProvider,
|
singleUserStateProvider,
|
||||||
globalStateProvider,
|
globalStateProvider,
|
||||||
new DefaultDerivedStateProvider(storageServiceProvider),
|
new DefaultDerivedStateProvider(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||||
|
@ -190,6 +191,7 @@ export class Main {
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
new MigrationBuilderService(),
|
new MigrationBuilderService(),
|
||||||
|
ClientType.Desktop,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
// TODO: this state service will have access to on disk storage, but not in memory storage.
|
||||||
|
|
|
@ -65,9 +65,10 @@ export class Menubar {
|
||||||
isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true;
|
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 =
|
const hasMasterPassword =
|
||||||
updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false;
|
updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false;
|
||||||
|
|
||||||
this.items = [
|
this.items = [
|
||||||
new FileMenu(
|
new FileMenu(
|
||||||
|
|
|
@ -103,7 +103,8 @@ export default {
|
||||||
isMacAppStore: isMacAppStore(),
|
isMacAppStore: isMacAppStore(),
|
||||||
isWindowsStore: isWindowsStore(),
|
isWindowsStore: isWindowsStore(),
|
||||||
reloadProcess: () => ipcRenderer.send("reload-process"),
|
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: (
|
openContextMenu: (
|
||||||
menu: {
|
menu: {
|
||||||
|
|
|
@ -164,7 +164,7 @@ export class DesktopSettingsService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the setting for whether or not the application should be shown in the dock.
|
* 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) {
|
async setAlwaysShowDock(value: boolean) {
|
||||||
await this.alwaysShowDockState.update(() => value);
|
await this.alwaysShowDockState.update(() => value);
|
||||||
|
|
|
@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService {
|
||||||
}
|
}
|
||||||
log.initialize();
|
log.initialize();
|
||||||
|
|
||||||
ipcMain.handle("ipc.log", (_event, { level, message }) => {
|
ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => {
|
||||||
this.write(level, message);
|
this.write(level, message, ...optionalParams);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
write(level: LogLevelType, message: string) {
|
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
|
||||||
if (this.filter != null && this.filter(level)) {
|
if (this.filter != null && this.filter(level)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LogLevelType.Debug:
|
case LogLevelType.Debug:
|
||||||
log.debug(message);
|
log.debug(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Info:
|
case LogLevelType.Info:
|
||||||
log.info(message);
|
log.info(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Warning:
|
case LogLevelType.Warning:
|
||||||
log.warn(message);
|
log.warn(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Error:
|
case LogLevelType.Error:
|
||||||
log.error(message);
|
log.error(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService {
|
||||||
super(ipc.platform.isDev, filter);
|
super(ipc.platform.isDev, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
write(level: LogLevelType, message: string) {
|
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
|
||||||
if (this.filter != null && this.filter(level)) {
|
if (this.filter != null && this.filter(level)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* 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 */
|
/* eslint-disable no-console */
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LogLevelType.Debug:
|
case LogLevelType.Debug:
|
||||||
console.debug(message);
|
console.debug(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Info:
|
case LogLevelType.Info:
|
||||||
console.info(message);
|
console.info(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Warning:
|
case LogLevelType.Warning:
|
||||||
console.warn(message);
|
console.warn(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
case LogLevelType.Error:
|
case LogLevelType.Error:
|
||||||
console.error(message);
|
console.error(message, ...optionalParams);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||||
groups: groups$,
|
groups: groups$,
|
||||||
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
|
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
@ -620,7 +619,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCollectionToAccessItemView(
|
function mapCollectionToAccessItemView(
|
||||||
collection: CollectionView,
|
collection: CollectionAdminView,
|
||||||
organization: Organization,
|
organization: Organization,
|
||||||
flexibleCollectionsV1Enabled: boolean,
|
flexibleCollectionsV1Enabled: boolean,
|
||||||
accessSelection?: CollectionAccessSelectionView,
|
accessSelection?: CollectionAccessSelectionView,
|
||||||
|
@ -632,7 +631,8 @@ function mapCollectionToAccessItemView(
|
||||||
labelName: collection.name,
|
labelName: collection.name,
|
||||||
listName: collection.name,
|
listName: collection.name,
|
||||||
readonly:
|
readonly:
|
||||||
group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled),
|
group !== undefined ||
|
||||||
|
!collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled),
|
||||||
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
||||||
viaGroupName: group?.name,
|
viaGroupName: group?.name,
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,12 +44,10 @@ export class AccountComponent {
|
||||||
|
|
||||||
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
|
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.FlexibleCollectionsMigration,
|
FeatureFlag.FlexibleCollectionsMigration,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.FlexibleCollectionsV1,
|
FeatureFlag.FlexibleCollectionsV1,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// FormGroup validators taken from server Organization domain object
|
// FormGroup validators taken from server Organization domain object
|
||||||
|
|
|
@ -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 { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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 { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
||||||
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.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 {
|
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||||
tabbedHeader = false;
|
tabbedHeader = false;
|
||||||
constructor(
|
constructor(
|
||||||
|
dialogService: DialogService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
messagingService: MessagingService,
|
messagingService: MessagingService,
|
||||||
|
@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
|
dialogService,
|
||||||
apiService,
|
apiService,
|
||||||
modalService,
|
modalService,
|
||||||
messagingService,
|
messagingService,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
|
||||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import * as jq from "jquery";
|
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 { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.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 { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.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";
|
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",
|
templateUrl: "app.component.html",
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnDestroy, OnInit {
|
export class AppComponent implements OnDestroy, OnInit {
|
||||||
private lastActivity: number = null;
|
private lastActivity: Date = null;
|
||||||
private idleTimer: number = null;
|
private idleTimer: number = null;
|
||||||
private isIdle = false;
|
private isIdle = false;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
@ -86,6 +87,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||||
private stateEventRunnerService: StateEventRunnerService,
|
private stateEventRunnerService: StateEventRunnerService,
|
||||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||||
private organizationService: InternalOrganizationServiceAbstraction,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -298,15 +300,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async recordActivity() {
|
private async recordActivity() {
|
||||||
const now = new Date().getTime();
|
const activeUserId = await firstValueFrom(
|
||||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
|
);
|
||||||
|
const now = new Date();
|
||||||
|
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastActivity = now;
|
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.
|
await this.accountService.setAccountActivity(activeUserId, now);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.stateService.setLastActive(now);
|
|
||||||
// Idle states
|
// Idle states
|
||||||
if (this.isIdle) {
|
if (this.isIdle) {
|
||||||
this.isIdle = false;
|
this.isIdle = false;
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class UserKeyRotationService {
|
||||||
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
||||||
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
||||||
|
|
||||||
if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) {
|
if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
|
||||||
await this.apiService.postUserKeyUpdate(request);
|
await this.apiService.postUserKeyUpdate(request);
|
||||||
} else {
|
} else {
|
||||||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
||||||
|
|
|
@ -15,13 +15,6 @@
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<app-two-factor-verify
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
[type]="type"
|
|
||||||
(onAuthed)="auth($any($event))"
|
|
||||||
*ngIf="!authed"
|
|
||||||
>
|
|
||||||
</app-two-factor-verify>
|
|
||||||
<form
|
<form
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
|
|
|
@ -15,13 +15,6 @@
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<app-two-factor-verify
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
[type]="type"
|
|
||||||
(onAuthed)="auth($any($event))"
|
|
||||||
*ngIf="!authed"
|
|
||||||
>
|
|
||||||
</app-two-factor-verify>
|
|
||||||
<form
|
<form
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
|
|
|
@ -15,13 +15,6 @@
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<app-two-factor-verify
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
[type]="type"
|
|
||||||
(onAuthed)="auth($any($event))"
|
|
||||||
*ngIf="!authed"
|
|
||||||
>
|
|
||||||
</app-two-factor-verify>
|
|
||||||
<form
|
<form
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
|
||||||
</app-two-factor-verify>
|
|
||||||
<ng-container *ngIf="authed">
|
<ng-container *ngIf="authed">
|
||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<ng-container *ngIf="code">
|
<ng-container *ngIf="code">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
|
@ -8,15 +8,23 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
|
||||||
|
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
|
||||||
|
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
|
||||||
|
import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
|
||||||
|
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
|
||||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||||
|
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
|
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
|
||||||
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
|
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
|
||||||
import { TwoFactorEmailComponent } from "./two-factor-email.component";
|
import { TwoFactorEmailComponent } from "./two-factor-email.component";
|
||||||
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
|
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
|
||||||
|
import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
|
||||||
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
|
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
|
||||||
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
|
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
|
||||||
|
|
||||||
|
@ -52,6 +60,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||||
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
|
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected dialogService: DialogService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
protected modalService: ModalService,
|
protected modalService: ModalService,
|
||||||
protected messagingService: MessagingService,
|
protected messagingService: MessagingService,
|
||||||
|
@ -114,50 +123,82 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callTwoFactorVerifyDialog(type?: TwoFactorProviderType) {
|
||||||
|
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
|
||||||
|
data: { type: type, organizationId: this.organizationId },
|
||||||
|
});
|
||||||
|
return await lastValueFrom(twoFactorVerifyDialogRef.closed);
|
||||||
|
}
|
||||||
|
|
||||||
async manage(type: TwoFactorProviderType) {
|
async manage(type: TwoFactorProviderType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TwoFactorProviderType.Authenticator: {
|
case TwoFactorProviderType.Authenticator: {
|
||||||
|
const result: AuthResponse<TwoFactorAuthenticatorResponse> =
|
||||||
|
await this.callTwoFactorVerifyDialog(type);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const authComp = await this.openModal(
|
const authComp = await this.openModal(
|
||||||
this.authenticatorModalRef,
|
this.authenticatorModalRef,
|
||||||
TwoFactorAuthenticatorComponent,
|
TwoFactorAuthenticatorComponent,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
await authComp.auth(result);
|
||||||
authComp.onUpdated.subscribe((enabled: boolean) => {
|
authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TwoFactorProviderType.Yubikey: {
|
case TwoFactorProviderType.Yubikey: {
|
||||||
|
const result: AuthResponse<TwoFactorYubiKeyResponse> =
|
||||||
|
await this.callTwoFactorVerifyDialog(type);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
yubiComp.auth(result);
|
||||||
yubiComp.onUpdated.subscribe((enabled: boolean) => {
|
yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
|
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TwoFactorProviderType.Duo: {
|
case TwoFactorProviderType.Duo: {
|
||||||
|
const result: AuthResponse<TwoFactorDuoResponse> =
|
||||||
|
await this.callTwoFactorVerifyDialog(type);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
duoComp.auth(result);
|
||||||
duoComp.onUpdated.subscribe((enabled: boolean) => {
|
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.Duo);
|
this.updateStatus(enabled, TwoFactorProviderType.Duo);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TwoFactorProviderType.Email: {
|
case TwoFactorProviderType.Email: {
|
||||||
|
const result: AuthResponse<TwoFactorEmailResponse> =
|
||||||
|
await this.callTwoFactorVerifyDialog(type);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
await emailComp.auth(result);
|
||||||
emailComp.onUpdated.subscribe((enabled: boolean) => {
|
emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TwoFactorProviderType.WebAuthn: {
|
case TwoFactorProviderType.WebAuthn: {
|
||||||
|
const result: AuthResponse<TwoFactorWebAuthnResponse> =
|
||||||
|
await this.callTwoFactorVerifyDialog(type);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const webAuthnComp = await this.openModal(
|
const webAuthnComp = await this.openModal(
|
||||||
this.webAuthnModalRef,
|
this.webAuthnModalRef,
|
||||||
TwoFactorWebAuthnComponent,
|
TwoFactorWebAuthnComponent,
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
webAuthnComp.auth(result);
|
||||||
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
|
webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
|
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -167,10 +208,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recoveryCode() {
|
async recoveryCode() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
const result = await this.callTwoFactorVerifyDialog(-1 as TwoFactorProviderType);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
if (result) {
|
||||||
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
|
const recoverComp = await this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
|
||||||
|
recoverComp.auth(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async premiumRequired() {
|
async premiumRequired() {
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<div class="modal-body">
|
<bit-dialog dialogSize="default">
|
||||||
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
|
<span bitDialogTitle>
|
||||||
</app-user-verification>
|
{{ "twoStepLogin" | i18n }}
|
||||||
</div>
|
<small class="tw-text-muted">{{ dialogTitle }}</small>
|
||||||
<div class="modal-footer">
|
</span>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<ng-container bitDialogContent>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<app-user-verification-form-input
|
||||||
<span>{{ "continue" | i18n }}</span>
|
formControlName="secret"
|
||||||
</button>
|
ngDefaultControl
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
name="secret"
|
||||||
{{ "close" | i18n }}
|
></app-user-verification-form-input>
|
||||||
</button>
|
</ng-container>
|
||||||
</div>
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||||
|
{{ "continue" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, EventEmitter, Inject, Output } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
@ -8,46 +10,74 @@ import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request
|
||||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||||
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
||||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
type TwoFactorVerifyDialogData = {
|
||||||
|
type: TwoFactorProviderType;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-two-factor-verify",
|
selector: "app-two-factor-verify",
|
||||||
templateUrl: "two-factor-verify.component.html",
|
templateUrl: "two-factor-verify.component.html",
|
||||||
})
|
})
|
||||||
export class TwoFactorVerifyComponent {
|
export class TwoFactorVerifyComponent {
|
||||||
@Input() type: TwoFactorProviderType;
|
type: TwoFactorProviderType;
|
||||||
@Input() organizationId: string;
|
organizationId: string;
|
||||||
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
||||||
|
|
||||||
secret: Verification;
|
|
||||||
formPromise: Promise<TwoFactorResponse>;
|
formPromise: Promise<TwoFactorResponse>;
|
||||||
|
|
||||||
|
protected formGroup = new FormGroup({
|
||||||
|
secret: new FormControl<Verification | null>(null),
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
|
||||||
|
private dialogRef: DialogRef,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private logService: LogService,
|
private i18nService: I18nService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
) {}
|
) {
|
||||||
|
this.type = data.type;
|
||||||
|
this.organizationId = data.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
async submit() {
|
submit = async () => {
|
||||||
let hashedSecret: string;
|
let hashedSecret: string;
|
||||||
|
this.formPromise = this.userVerificationService
|
||||||
try {
|
.buildRequest(this.formGroup.value.secret)
|
||||||
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
|
.then((request) => {
|
||||||
hashedSecret =
|
hashedSecret =
|
||||||
this.secret.type === VerificationType.MasterPassword
|
this.formGroup.value.secret.type === VerificationType.MasterPassword
|
||||||
? request.masterPasswordHash
|
? request.masterPasswordHash
|
||||||
: request.otp;
|
: request.otp;
|
||||||
return this.apiCall(request);
|
return this.apiCall(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
this.onAuthed.emit({
|
this.dialogRef.close({
|
||||||
response: response,
|
response: response,
|
||||||
secret: hashedSecret,
|
secret: hashedSecret,
|
||||||
verificationType: this.secret.type,
|
verificationType: this.formGroup.value.secret.type,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
};
|
||||||
this.logService.error(e);
|
|
||||||
|
get dialogTitle(): string {
|
||||||
|
switch (this.type) {
|
||||||
|
case -1 as TwoFactorProviderType:
|
||||||
|
return this.i18nService.t("recoveryCodeTitle");
|
||||||
|
case TwoFactorProviderType.Duo:
|
||||||
|
return "Duo";
|
||||||
|
case TwoFactorProviderType.Email:
|
||||||
|
return this.i18nService.t("emailTitle");
|
||||||
|
case TwoFactorProviderType.WebAuthn:
|
||||||
|
return this.i18nService.t("webAuthnTitle");
|
||||||
|
case TwoFactorProviderType.Authenticator:
|
||||||
|
return this.i18nService.t("authenticatorAppTitle");
|
||||||
|
case TwoFactorProviderType.Yubikey:
|
||||||
|
return "Yubikey";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,4 +102,8 @@ export class TwoFactorVerifyComponent {
|
||||||
return this.apiService.getTwoFactorYubiKey(request);
|
return this.apiService.getTwoFactorYubiKey(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) {
|
||||||
|
return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,6 @@
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<app-two-factor-verify
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
[type]="type"
|
|
||||||
(onAuthed)="auth($any($event))"
|
|
||||||
*ngIf="!authed"
|
|
||||||
>
|
|
||||||
</app-two-factor-verify>
|
|
||||||
<form
|
<form
|
||||||
#form
|
#form
|
||||||
(ngSubmit)="submit()"
|
(ngSubmit)="submit()"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue