Compare commits
36 Commits
ad8bb930c5
...
d748d006b8
Author | SHA1 | Date |
---|---|---|
nick-livefront | d748d006b8 | |
nick-livefront | 6e49eeaf09 | |
nick-livefront | 68792b81b3 | |
nick-livefront | 749fcdfb33 | |
nick-livefront | bd54d8713d | |
nick-livefront | 7c622e8e59 | |
nick-livefront | 6dd95d60f6 | |
nick-livefront | dafaf01b39 | |
Cesar Gonzalez | 5dc200577c | |
Justin Baur | a8e4366ec0 | |
Matt Gibson | 089f251a0c | |
renovate[bot] | b3242145f9 | |
Justin Baur | b482a15d34 | |
nick-livefront | 7714118116 | |
nick-livefront | 5a5f9ee147 | |
nick-livefront | 5c27e8dc84 | |
Matt Gibson | a4f1a3f13d | |
Matt Gibson | 4c1c09f07f | |
Robyn MacCallum | 67280f48dd | |
Justin Baur | a7958c1a56 | |
Jared Snider | 7f5efcc18c | |
Justin Baur | acea273f97 | |
Victoria League | ec37e5e4d3 | |
KiruthigaManivannan | 2fa4c6e4f9 | |
github-actions[bot] | 11ba8d188d | |
Oscar Hinton | 14b2eb99a2 | |
github-actions[bot] | c7fa376be3 | |
github-actions[bot] | 788bef6b7a | |
Thomas Rittson | c21a58f2fb | |
Cesar Gonzalez | c3d4c7aa3d | |
Jake Fink | 2ff3fa92fb | |
SmithThe4th | d8749a0c56 | |
Jake Fink | 8afe915be1 | |
Alex Morask | cbf7c292f3 | |
Matt Gibson | e516eec200 | |
Ike | 1e4158fd87 |
|
@ -164,6 +164,10 @@ jobs:
|
|||
run: npm run dist:mv3
|
||||
working-directory: browser-source/apps/browser
|
||||
|
||||
- name: Build Chrome Manifest v3 Beta
|
||||
run: npm run dist:chrome:beta
|
||||
working-directory: browser-source/apps/browser
|
||||
|
||||
- name: Gulp
|
||||
run: gulp ci
|
||||
working-directory: browser-source/apps/browser
|
||||
|
@ -196,6 +200,13 @@ jobs:
|
|||
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
|
||||
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Firefox artifact
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
|
|
|
@ -9,6 +9,8 @@ const config: StorybookConfig = {
|
|||
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../apps/web/src/**/*.mdx",
|
||||
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../apps/browser/src/**/*.mdx",
|
||||
"../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../bitwarden_license/bit-web/src/**/*.mdx",
|
||||
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "chrome"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],
|
||||
"exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"],
|
||||
"files": [
|
||||
"./typings.d.ts",
|
||||
"./preview.tsx",
|
||||
"../libs/components/src/main.ts",
|
||||
"../libs/components/src/polyfills.ts"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
|
@ -35,6 +35,9 @@ function buildString() {
|
|||
if (process.env.MANIFEST_VERSION) {
|
||||
build = `-mv${process.env.MANIFEST_VERSION}`;
|
||||
}
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
build += "-beta";
|
||||
}
|
||||
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
|
||||
build = `-${process.env.BUILD_NUMBER}`;
|
||||
}
|
||||
|
@ -65,6 +68,9 @@ function distFirefox() {
|
|||
manifest.optional_permissions = manifest.optional_permissions.filter(
|
||||
(permission) => permission !== "privacy",
|
||||
);
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
manifest = applyBetaLabels(manifest);
|
||||
}
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
@ -72,6 +78,9 @@ function distFirefox() {
|
|||
function distOpera() {
|
||||
return dist("opera", (manifest) => {
|
||||
delete manifest.applications;
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
manifest = applyBetaLabels(manifest);
|
||||
}
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
@ -81,6 +90,9 @@ function distChrome() {
|
|||
delete manifest.applications;
|
||||
delete manifest.sidebar_action;
|
||||
delete manifest.commands._execute_sidebar_action;
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
manifest = applyBetaLabels(manifest);
|
||||
}
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
@ -90,6 +102,9 @@ function distEdge() {
|
|||
delete manifest.applications;
|
||||
delete manifest.sidebar_action;
|
||||
delete manifest.commands._execute_sidebar_action;
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
manifest = applyBetaLabels(manifest);
|
||||
}
|
||||
return manifest;
|
||||
});
|
||||
}
|
||||
|
@ -210,6 +225,9 @@ async function safariCopyBuild(source, dest) {
|
|||
delete manifest.commands._execute_sidebar_action;
|
||||
delete manifest.optional_permissions;
|
||||
manifest.permissions.push("nativeMessaging");
|
||||
if (process.env.BETA_BUILD === "1") {
|
||||
manifest = applyBetaLabels(manifest);
|
||||
}
|
||||
return manifest;
|
||||
}),
|
||||
),
|
||||
|
@ -235,6 +253,19 @@ async function ciCoverage(cb) {
|
|||
.pipe(gulp.dest(paths.coverage));
|
||||
}
|
||||
|
||||
function applyBetaLabels(manifest) {
|
||||
manifest.name = "Bitwarden Password Manager BETA";
|
||||
manifest.short_name = "Bitwarden BETA";
|
||||
manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN.";
|
||||
if (process.env.GITHUB_RUN_ID) {
|
||||
manifest.version_name = `${manifest.version} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`;
|
||||
manifest.version = `${manifest.version}.${parseInt(process.env.GITHUB_RUN_ID.slice(-4))}`;
|
||||
} else {
|
||||
manifest.version = `${manifest.version}.0`;
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
exports["dist:firefox"] = distFirefox;
|
||||
exports["dist:chrome"] = distChrome;
|
||||
exports["dist:opera"] = distOpera;
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
"build:watch": "webpack --watch",
|
||||
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack",
|
||||
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
||||
"dist": "npm run build:prod && gulp dist",
|
||||
"dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist",
|
||||
"dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist",
|
||||
"dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist",
|
||||
"dist:chrome": "npm run build:prod && gulp dist:chrome",
|
||||
"dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome",
|
||||
"dist:firefox": "npm run build:prod && gulp dist:firefox",
|
||||
"dist:opera": "npm run build:prod && gulp dist:opera",
|
||||
"dist:safari": "npm run build:prod && gulp dist:safari",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||
},
|
||||
"extDesc": {
|
||||
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
|
||||
"message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje",
|
||||
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||
},
|
||||
"loginOrCreateNewAccount": {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||
},
|
||||
"extDesc": {
|
||||
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
|
||||
"message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais",
|
||||
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||
},
|
||||
"loginOrCreateNewAccount": {
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"message": "Bitwarden"
|
||||
},
|
||||
"extName": {
|
||||
"message": "Bitwarden Password Manager",
|
||||
"message": "Bitwarden - Trình Quản lý Mật khẩu",
|
||||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||
},
|
||||
"extDesc": {
|
||||
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
|
||||
"message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn",
|
||||
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
|
||||
},
|
||||
"loginOrCreateNewAccount": {
|
||||
|
@ -650,7 +650,7 @@
|
|||
"message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên."
|
||||
},
|
||||
"addLoginNotificationDescAlt": {
|
||||
"message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts."
|
||||
"message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"showCardsCurrentTab": {
|
||||
"message": "Hiển thị thẻ trên trang Tab"
|
||||
|
@ -685,13 +685,13 @@
|
|||
"message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web."
|
||||
},
|
||||
"changedPasswordNotificationDescAlt": {
|
||||
"message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts."
|
||||
"message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"enableUsePasskeys": {
|
||||
"message": "Ask to save and use passkeys"
|
||||
"message": "Đưa ra lựa chọn để lưu và sử dụng passkey"
|
||||
},
|
||||
"usePasskeysDesc": {
|
||||
"message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts."
|
||||
"message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"notificationChangeDesc": {
|
||||
"message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?"
|
||||
|
@ -712,7 +712,7 @@
|
|||
"message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. "
|
||||
},
|
||||
"contextMenuItemDescAlt": {
|
||||
"message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts."
|
||||
"message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"defaultUriMatchDetection": {
|
||||
"message": "Phương thức kiểm tra URI mặc định",
|
||||
|
@ -728,7 +728,7 @@
|
|||
"message": "Thay đổi màu sắc ứng dụng."
|
||||
},
|
||||
"themeDescAlt": {
|
||||
"message": "Change the application's color theme. Applies to all logged in accounts."
|
||||
"message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"dark": {
|
||||
"message": "Tối",
|
||||
|
@ -1061,10 +1061,10 @@
|
|||
"message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột."
|
||||
},
|
||||
"turnOffBrowserBuiltInPasswordManagerSettingsLink": {
|
||||
"message": "Edit browser settings."
|
||||
"message": "Thay đổi cài đặt của trình duyệt."
|
||||
},
|
||||
"autofillOverlayVisibilityOff": {
|
||||
"message": "Off",
|
||||
"message": "Tắt",
|
||||
"description": "Overlay setting select option for disabling autofill overlay"
|
||||
},
|
||||
"autofillOverlayVisibilityOnFieldFocus": {
|
||||
|
@ -1168,7 +1168,7 @@
|
|||
"message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập."
|
||||
},
|
||||
"faviconDescAlt": {
|
||||
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
|
||||
"message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị."
|
||||
},
|
||||
"enableBadgeCounter": {
|
||||
"message": "Hiển thị biểu tượng bộ đếm"
|
||||
|
@ -1500,7 +1500,7 @@
|
|||
"message": "Mã PIN không hợp lệ."
|
||||
},
|
||||
"tooManyInvalidPinEntryAttemptsLoggingOut": {
|
||||
"message": "Too many invalid PIN entry attempts. Logging out."
|
||||
"message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất."
|
||||
},
|
||||
"unlockWithBiometrics": {
|
||||
"message": "Mở khóa bằng sinh trắc học"
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
|
||||
|
||||
import {
|
||||
FactoryOptions,
|
||||
CachedServices,
|
||||
factory,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
StateProviderInitOptions,
|
||||
stateProviderFactory,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
type KdfConfigServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions;
|
||||
|
||||
export function kdfConfigServiceFactory(
|
||||
cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices,
|
||||
opts: KdfConfigServiceInitOptions,
|
||||
): Promise<AbstractKdfConfigService> {
|
||||
return factory(
|
||||
cache,
|
||||
"kdfConfigService",
|
||||
opts,
|
||||
async () => new KdfConfigService(await stateProviderFactory(cache, opts)),
|
||||
);
|
||||
}
|
|
@ -68,6 +68,7 @@ import {
|
|||
deviceTrustServiceFactory,
|
||||
DeviceTrustServiceInitOptions,
|
||||
} from "./device-trust-service.factory";
|
||||
import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory";
|
||||
import {
|
||||
keyConnectorServiceFactory,
|
||||
KeyConnectorServiceInitOptions,
|
||||
|
@ -106,7 +107,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
|
|||
AuthRequestServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
GlobalStateProviderInitOptions &
|
||||
BillingAccountProfileStateServiceInitOptions;
|
||||
BillingAccountProfileStateServiceInitOptions &
|
||||
KdfConfigServiceInitOptions;
|
||||
|
||||
export function loginStrategyServiceFactory(
|
||||
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
|
||||
|
@ -140,6 +142,7 @@ export function loginStrategyServiceFactory(
|
|||
await internalUserDecryptionOptionServiceFactory(cache, opts),
|
||||
await globalStateProviderFactory(cache, opts),
|
||||
await billingAccountProfileStateServiceFactory(cache, opts),
|
||||
await kdfConfigServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,13 +22,16 @@ import {
|
|||
stateServiceFactory,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
|
||||
|
||||
type PinCryptoServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions &
|
||||
StateServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
VaultTimeoutSettingsServiceInitOptions &
|
||||
LogServiceInitOptions;
|
||||
LogServiceInitOptions &
|
||||
KdfConfigServiceInitOptions;
|
||||
|
||||
export function pinCryptoServiceFactory(
|
||||
cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices,
|
||||
|
@ -44,6 +47,7 @@ export function pinCryptoServiceFactory(
|
|||
await cryptoServiceFactory(cache, opts),
|
||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||
await logServiceFactory(cache, opts),
|
||||
await kdfConfigServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
import {
|
||||
FactoryOptions,
|
||||
CachedServices,
|
||||
factory,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory";
|
||||
import {
|
||||
I18nServiceInitOptions,
|
||||
i18nServiceFactory,
|
||||
|
@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions;
|
|||
|
||||
export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions &
|
||||
I18nServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions;
|
||||
PlatformUtilsServiceInitOptions &
|
||||
GlobalStateProvider;
|
||||
|
||||
export async function twoFactorServiceFactory(
|
||||
cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices,
|
||||
|
@ -33,6 +36,7 @@ export async function twoFactorServiceFactory(
|
|||
new TwoFactorService(
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
await globalStateProviderFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
service.init();
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
|
||||
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
|
||||
import {
|
||||
internalMasterPasswordServiceFactory,
|
||||
MasterPasswordServiceInitOptions,
|
||||
|
@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
|
|||
PinCryptoServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
VaultTimeoutSettingsServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions;
|
||||
PlatformUtilsServiceInitOptions &
|
||||
KdfConfigServiceInitOptions;
|
||||
|
||||
export function userVerificationServiceFactory(
|
||||
cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices,
|
||||
|
@ -82,6 +84,7 @@ export function userVerificationServiceFactory(
|
|||
await logServiceFactory(cache, opts),
|
||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
await kdfConfigServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
|||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
private routerService: BrowserRouterService,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
|
@ -90,6 +92,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
kdfConfigService,
|
||||
);
|
||||
this.successRoute = "/tabs/current";
|
||||
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
|
|
@ -2,7 +2,10 @@ import { Component } from "@angular/core";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import {
|
||||
TwoFactorProviderDetails,
|
||||
TwoFactorService,
|
||||
} from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
|||
this.navigateTo2FA();
|
||||
}
|
||||
|
||||
choose(p: any) {
|
||||
super.choose(p);
|
||||
this.twoFactorService.setSelectedProvider(p.type);
|
||||
override async choose(p: TwoFactorProviderDetails) {
|
||||
await super.choose(p);
|
||||
await this.twoFactorService.setSelectedProvider(p.type);
|
||||
|
||||
this.navigateTo2FA();
|
||||
}
|
||||
|
|
|
@ -130,7 +130,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
|
||||
if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) {
|
||||
injectedScripts.push("autofiller.js");
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!triggeringOnPageLoad) {
|
||||
await this.scriptInjectorService.inject({
|
||||
tabId: tab.id,
|
||||
injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" },
|
||||
|
|
|
@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs";
|
|||
import {
|
||||
PinCryptoServiceAbstraction,
|
||||
PinCryptoService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
AuthRequestServiceAbstraction,
|
||||
|
@ -33,11 +31,11 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut
|
|||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
@ -48,11 +46,11 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
|||
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import {
|
||||
|
@ -275,7 +273,6 @@ export default class MainBackground {
|
|||
containerService: ContainerService;
|
||||
auditService: AuditServiceAbstraction;
|
||||
authService: AuthServiceAbstraction;
|
||||
loginStrategyService: LoginStrategyServiceAbstraction;
|
||||
loginEmailService: LoginEmailServiceAbstraction;
|
||||
importApiService: ImportApiServiceAbstraction;
|
||||
importService: ImportServiceAbstraction;
|
||||
|
@ -299,7 +296,6 @@ export default class MainBackground {
|
|||
providerService: ProviderServiceAbstraction;
|
||||
keyConnectorService: KeyConnectorServiceAbstraction;
|
||||
userVerificationService: UserVerificationServiceAbstraction;
|
||||
twoFactorService: TwoFactorServiceAbstraction;
|
||||
vaultFilterService: VaultFilterService;
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
||||
encryptService: EncryptService;
|
||||
|
@ -339,6 +335,7 @@ export default class MainBackground {
|
|||
intraprocessMessagingSubject: Subject<Message<object>>;
|
||||
userKeyInitService: UserKeyInitService;
|
||||
scriptInjectorService: BrowserScriptInjectorService;
|
||||
kdfConfigService: kdfConfigServiceAbstraction;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
|
@ -493,7 +490,7 @@ export default class MainBackground {
|
|||
this.accountService,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
|
||||
this.derivedStateProvider = new BackgroundDerivedStateProvider();
|
||||
this.stateProvider = new DefaultStateProvider(
|
||||
this.activeUserStateProvider,
|
||||
this.singleUserStateProvider,
|
||||
|
@ -542,6 +539,9 @@ export default class MainBackground {
|
|||
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
|
||||
|
||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||
|
||||
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
||||
|
||||
this.cryptoService = new BrowserCryptoService(
|
||||
this.masterPasswordService,
|
||||
this.keyGenerationService,
|
||||
|
@ -553,6 +553,7 @@ export default class MainBackground {
|
|||
this.accountService,
|
||||
this.stateProvider,
|
||||
this.biometricStateService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||
|
@ -607,8 +608,6 @@ export default class MainBackground {
|
|||
this.stateService,
|
||||
);
|
||||
|
||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
|
@ -652,31 +651,6 @@ export default class MainBackground {
|
|||
|
||||
this.loginEmailService = new LoginEmailService(this.stateProvider);
|
||||
|
||||
this.loginStrategyService = new LoginStrategyService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.keyConnectorService,
|
||||
this.environmentService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.i18nService,
|
||||
this.encryptService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this.deviceTrustService,
|
||||
this.authRequestService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.globalStateProvider,
|
||||
this.billingAccountProfileStateService,
|
||||
);
|
||||
|
||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||
|
||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||
|
@ -725,6 +699,7 @@ export default class MainBackground {
|
|||
this.cryptoService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.logService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.userVerificationService = new UserVerificationService(
|
||||
|
@ -739,6 +714,7 @@ export default class MainBackground {
|
|||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.platformUtilsService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.vaultFilterService = new VaultFilterService(
|
||||
|
@ -861,7 +837,7 @@ export default class MainBackground {
|
|||
this.cipherService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.stateService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.organizationVaultExportService = new OrganizationVaultExportService(
|
||||
|
@ -869,8 +845,8 @@ export default class MainBackground {
|
|||
this.apiService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.stateService,
|
||||
this.collectionService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.exportService = new VaultExportService(
|
||||
|
@ -1104,8 +1080,7 @@ export default class MainBackground {
|
|||
this.userKeyInitService.listenForActiveUserChangesToSetUserKey();
|
||||
|
||||
await (this.i18nService as I18nService).init();
|
||||
await (this.eventUploadService as EventUploadService).init(true);
|
||||
this.twoFactorService.init();
|
||||
(this.eventUploadService as EventUploadService).init(true);
|
||||
|
||||
if (this.popupOnlyContext) {
|
||||
return;
|
||||
|
|
|
@ -4,6 +4,10 @@ import {
|
|||
AccountServiceInitOptions,
|
||||
accountServiceFactory,
|
||||
} from "../../../auth/background/service-factories/account-service.factory";
|
||||
import {
|
||||
KdfConfigServiceInitOptions,
|
||||
kdfConfigServiceFactory,
|
||||
} from "../../../auth/background/service-factories/kdf-config-service.factory";
|
||||
import {
|
||||
internalMasterPasswordServiceFactory,
|
||||
MasterPasswordServiceInitOptions,
|
||||
|
@ -18,7 +22,10 @@ import {
|
|||
} from "../../background/service-factories/log-service.factory";
|
||||
import { BrowserCryptoService } from "../../services/browser-crypto.service";
|
||||
|
||||
import { biometricStateServiceFactory } from "./biometric-state-service.factory";
|
||||
import {
|
||||
BiometricStateServiceInitOptions,
|
||||
biometricStateServiceFactory,
|
||||
} from "./biometric-state-service.factory";
|
||||
import {
|
||||
cryptoFunctionServiceFactory,
|
||||
CryptoFunctionServiceInitOptions,
|
||||
|
@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
|
|||
LogServiceInitOptions &
|
||||
StateServiceInitOptions &
|
||||
AccountServiceInitOptions &
|
||||
StateProviderInitOptions;
|
||||
StateProviderInitOptions &
|
||||
BiometricStateServiceInitOptions &
|
||||
KdfConfigServiceInitOptions;
|
||||
|
||||
export function cryptoServiceFactory(
|
||||
cache: { cryptoService?: AbstractCryptoService } & CachedServices,
|
||||
|
@ -68,6 +77,7 @@ export function cryptoServiceFactory(
|
|||
await accountServiceFactory(cache, opts),
|
||||
await stateProviderFactory(cache, opts),
|
||||
await biometricStateServiceFactory(cache, opts),
|
||||
await kdfConfigServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state";
|
|||
import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider";
|
||||
|
||||
import { CachedServices, FactoryOptions, factory } from "./factory-options";
|
||||
import {
|
||||
StorageServiceProviderInitOptions,
|
||||
storageServiceProviderFactory,
|
||||
} from "./storage-service-provider.factory";
|
||||
|
||||
type DerivedStateProviderFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
|
||||
StorageServiceProviderInitOptions;
|
||||
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions;
|
||||
|
||||
export async function derivedStateProviderFactory(
|
||||
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
|
||||
|
@ -21,7 +16,6 @@ export async function derivedStateProviderFactory(
|
|||
cache,
|
||||
"derivedStateProvider",
|
||||
opts,
|
||||
async () =>
|
||||
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
|
||||
async () => new BackgroundDerivedStateProvider(),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -238,10 +238,6 @@ export class BrowserApi {
|
|||
return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage();
|
||||
}
|
||||
|
||||
static getApplicationVersion(): string {
|
||||
return chrome.runtime.getManifest().version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension views that match the given properties. This method is not
|
||||
* available within background service worker. As a result, it will return an
|
||||
|
|
|
@ -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,367 @@
|
|||
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,
|
||||
ButtonModule,
|
||||
I18nMockService,
|
||||
IconButtonModule,
|
||||
} 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: `
|
||||
<div class="tw-mb-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item last item</div>
|
||||
`,
|
||||
standalone: true,
|
||||
})
|
||||
class VaultComponent {}
|
||||
|
||||
@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",
|
||||
},
|
||||
];
|
||||
}
|
|
@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService
|
|||
async save(key: string, obj: any): Promise<void> {
|
||||
obj = objToStore(obj);
|
||||
|
||||
if (obj == null) {
|
||||
// Safari does not support set of null values
|
||||
return this.remove(key);
|
||||
}
|
||||
|
||||
const keyedObj = { [key]: obj };
|
||||
return new Promise<void>((resolve) => {
|
||||
this.chromeStorageApi.set(keyedObj, () => {
|
||||
|
|
|
@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => {
|
|||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("removes the key when the value is null", async () => {
|
||||
const removeMock = chrome.storage.local.remove as jest.Mock;
|
||||
removeMock.mockImplementation((key, callback) => {
|
||||
delete store[key];
|
||||
callback();
|
||||
});
|
||||
const key = "key";
|
||||
await service.save(key, null);
|
||||
expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService {
|
|||
accountService: AccountService,
|
||||
stateProvider: StateProvider,
|
||||
private biometricStateService: BiometricStateService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
|
@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService {
|
|||
stateService,
|
||||
accountService,
|
||||
stateProvider,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> {
|
||||
|
|
|
@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||
}
|
||||
|
||||
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> {
|
||||
return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim();
|
||||
getApplicationVersionNumber(): Promise<string> {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim());
|
||||
}
|
||||
|
||||
supportsWebAuthn(win: Window): boolean {
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||
|
@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
|
|||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
||||
): DerivedState<TTo> {
|
||||
const [cacheKey, storageService] = storageLocation;
|
||||
return new BackgroundDerivedState(
|
||||
parentState$,
|
||||
deriveDefinition,
|
||||
storageService,
|
||||
cacheKey,
|
||||
deriveDefinition.buildCacheKey(),
|
||||
dependencies,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { Observable, Subscription } from "rxjs";
|
||||
import { Observable, Subscription, concatMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state";
|
||||
|
@ -22,11 +19,10 @@ export class BackgroundDerivedState<
|
|||
constructor(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
memoryStorage: AbstractStorageService & ObservableStorageService,
|
||||
portName: string,
|
||||
dependencies: TDeps,
|
||||
) {
|
||||
super(parentState$, deriveDefinition, memoryStorage, dependencies);
|
||||
super(parentState$, deriveDefinition, dependencies);
|
||||
|
||||
// listen for foreground derived states to connect
|
||||
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||
|
@ -42,7 +38,20 @@ export class BackgroundDerivedState<
|
|||
});
|
||||
port.onMessage.addListener(listenerCallback);
|
||||
|
||||
const stateSubscription = this.state$.subscribe();
|
||||
const stateSubscription = this.state$
|
||||
.pipe(
|
||||
concatMap(async (state) => {
|
||||
await this.sendMessage(
|
||||
{
|
||||
action: "nextState",
|
||||
data: JSON.stringify(state),
|
||||
id: Utils.newGuid(),
|
||||
},
|
||||
port,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.portSubscriptions.set(port, stateSubscription);
|
||||
});
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
*/
|
||||
|
||||
import { NgZone } from "@angular/core";
|
||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
||||
import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition
|
||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
||||
import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec";
|
||||
|
||||
import { mockPorts } from "../../../spec/mock-port.spec-util";
|
||||
|
||||
|
@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory");
|
|||
const deriveDefinition = new DeriveDefinition(stateDefinition, "test", {
|
||||
derive: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||
deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)),
|
||||
cleanupDelayMs: 1000,
|
||||
});
|
||||
|
||||
// Mock out the runInsideAngular operator so we don't have to deal with zone.js
|
||||
|
@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => {
|
|||
let foreground: ForegroundDerivedState<Date>;
|
||||
let background: BackgroundDerivedState<string, Date, Record<string, unknown>>;
|
||||
let parentState$: Subject<string>;
|
||||
let memoryStorage: FakeStorageService;
|
||||
const initialParent = "2020-01-01";
|
||||
const ngZone = mock<NgZone>();
|
||||
const portName = "testPort";
|
||||
|
@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => {
|
|||
beforeEach(() => {
|
||||
mockPorts();
|
||||
parentState$ = new Subject<string>();
|
||||
memoryStorage = new FakeStorageService();
|
||||
|
||||
background = new BackgroundDerivedState(
|
||||
parentState$,
|
||||
deriveDefinition,
|
||||
memoryStorage,
|
||||
portName,
|
||||
{},
|
||||
);
|
||||
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
||||
background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {});
|
||||
foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => {
|
|||
});
|
||||
|
||||
it("should initialize a late-connected foreground", async () => {
|
||||
const newForeground = new ForegroundDerivedState(
|
||||
deriveDefinition,
|
||||
memoryStorage,
|
||||
portName,
|
||||
ngZone,
|
||||
);
|
||||
const backgroundEmissions = trackEmissions(background.state$);
|
||||
const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
const backgroundTracker = new ObservableTracker(background.state$);
|
||||
parentState$.next(initialParent);
|
||||
await awaitAsync();
|
||||
const foregroundTracker = new ObservableTracker(newForeground.state$);
|
||||
|
||||
const foregroundEmissions = trackEmissions(newForeground.state$);
|
||||
await awaitAsync(10);
|
||||
|
||||
expect(backgroundEmissions).toEqual([new Date(initialParent)]);
|
||||
expect(foregroundEmissions).toEqual([new Date(initialParent)]);
|
||||
expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||
expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent));
|
||||
});
|
||||
|
||||
describe("forceValue", () => {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { NgZone } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
|
||||
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
|
||||
|
@ -14,19 +9,18 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state";
|
|||
import { ForegroundDerivedState } from "./foreground-derived-state";
|
||||
|
||||
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
|
||||
constructor(
|
||||
storageServiceProvider: StorageServiceProvider,
|
||||
private ngZone: NgZone,
|
||||
) {
|
||||
super(storageServiceProvider);
|
||||
constructor(private ngZone: NgZone) {
|
||||
super();
|
||||
}
|
||||
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||
_parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
_dependencies: TDeps,
|
||||
storageLocation: [string, AbstractStorageService & ObservableStorageService],
|
||||
): DerivedState<TTo> {
|
||||
const [cacheKey, storageService] = storageLocation;
|
||||
return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone);
|
||||
return new ForegroundDerivedState(
|
||||
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 { awaitAsync, trackEmissions } from "@bitwarden/common/../spec";
|
||||
import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service";
|
||||
import { awaitAsync } from "@bitwarden/common/../spec";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { DeriveDefinition } from "@bitwarden/common/platform/state";
|
||||
|
@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => {
|
|||
|
||||
describe("ForegroundDerivedState", () => {
|
||||
let sut: ForegroundDerivedState<Date>;
|
||||
let memoryStorage: FakeStorageService;
|
||||
const portName = "testPort";
|
||||
const ngZone = mock<NgZone>();
|
||||
|
||||
beforeEach(() => {
|
||||
memoryStorage = new FakeStorageService();
|
||||
memoryStorage.internalUpdateValuesRequireDeserialization(true);
|
||||
mockPorts();
|
||||
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
|
||||
sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => {
|
|||
expect(disconnectSpy).toHaveBeenCalled();
|
||||
expect(sut["port"]).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit when the memory storage updates", async () => {
|
||||
const dateString = "2020-01-01";
|
||||
const emissions = trackEmissions(sut.state$);
|
||||
|
||||
await memoryStorage.save(deriveDefinition.storageKey, {
|
||||
derived: true,
|
||||
value: new Date(dateString),
|
||||
});
|
||||
|
||||
await awaitAsync();
|
||||
|
||||
expect(emissions).toEqual([new Date(dateString)]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,19 +6,14 @@ import {
|
|||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
share,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from "rxjs";
|
||||
import { Jsonify, JsonObject } from "type-fest";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
|
||||
import { DerivedStateDependencies } from "@bitwarden/common/types/state";
|
||||
|
@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
|
|||
import { runInsideAngular } from "../browser/run-inside-angular.operator";
|
||||
|
||||
export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
||||
private storageKey: string;
|
||||
private port: chrome.runtime.Port;
|
||||
private backgroundResponses$: Observable<DerivedStateMessage>;
|
||||
state$: Observable<TTo>;
|
||||
|
||||
constructor(
|
||||
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
|
||||
private memoryStorage: AbstractStorageService & ObservableStorageService,
|
||||
private portName: string,
|
||||
private ngZone: NgZone,
|
||||
) {
|
||||
this.storageKey = deriveDefinition.storageKey;
|
||||
|
||||
const initialStorageGet$ = defer(() => {
|
||||
return this.getStoredValue();
|
||||
}).pipe(
|
||||
filter((s) => s.derived),
|
||||
map((s) => s.value),
|
||||
);
|
||||
|
||||
const latestStorage$ = this.memoryStorage.updates$.pipe(
|
||||
filter((s) => s.key === this.storageKey),
|
||||
switchMap(async (storageUpdate) => {
|
||||
if (storageUpdate.updateType === "remove") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.getStoredValue();
|
||||
}),
|
||||
filter((s) => s.derived),
|
||||
map((s) => s.value),
|
||||
);
|
||||
const latestValueFromPort$ = (port: chrome.runtime.Port) => {
|
||||
return fromChromeEvent(port.onMessage).pipe(
|
||||
map(([message]) => message as DerivedStateMessage),
|
||||
filter((message) => message.originator === "background" && message.action === "nextState"),
|
||||
map((message) => {
|
||||
const json = JSON.parse(message.data) as Jsonify<TTo>;
|
||||
return this.deriveDefinition.deserialize(json);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
this.state$ = defer(() => of(this.initializePort())).pipe(
|
||||
switchMap(() => merge(initialStorageGet$, latestStorage$)),
|
||||
switchMap(() => latestValueFromPort$(this.port)),
|
||||
share({
|
||||
connector: () => new ReplaySubject<TTo>(1),
|
||||
resetOnRefCountZero: () =>
|
||||
|
@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
|
|||
this.port = null;
|
||||
this.backgroundResponses$ = null;
|
||||
}
|
||||
|
||||
protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> {
|
||||
if (this.memoryStorage.valuesRequireDeserialization) {
|
||||
const storedJson = await this.memoryStorage.get<
|
||||
Jsonify<{ derived: true; value: JsonObject }>
|
||||
>(this.storageKey);
|
||||
|
||||
if (!storedJson?.derived) {
|
||||
return { derived: false, value: null };
|
||||
}
|
||||
|
||||
const value = this.deriveDefinition.deserialize(storedJson.value as any);
|
||||
|
||||
return { derived: true, value };
|
||||
} else {
|
||||
const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey);
|
||||
|
||||
if (!stored?.derived) {
|
||||
return { derived: false, value: null };
|
||||
}
|
||||
|
||||
return { derived: true, value: stored.value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
|||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { HeaderComponent } from "../platform/popup/header.component";
|
||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||
|
@ -108,6 +112,10 @@ import "../platform/popup/locales";
|
|||
AccountComponent,
|
||||
ButtonModule,
|
||||
ExportScopeCalloutComponent,
|
||||
PopupPageComponent,
|
||||
PopupTabNavigationComponent,
|
||||
PopupFooterComponent,
|
||||
PopupHeaderComponent,
|
||||
],
|
||||
declarations: [
|
||||
ActionButtonsComponent,
|
||||
|
|
|
@ -68,7 +68,7 @@ img {
|
|||
border: none;
|
||||
}
|
||||
|
||||
a {
|
||||
a:not(popup-page a, popup-tab-navigation a) {
|
||||
text-decoration: none;
|
||||
|
||||
@include themify($themes) {
|
||||
|
@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
|
|||
}
|
||||
}
|
||||
|
||||
header:not(bit-callout header, bit-dialog header) {
|
||||
header:not(bit-callout header, bit-dialog header, popup-page header) {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
|
||||
|
@ -448,7 +448,7 @@ app-root {
|
|||
}
|
||||
}
|
||||
|
||||
main {
|
||||
main:not(popup-page main) {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
bottom: 0;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common";
|
|||
import { Inject, Injectable } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
@ -15,6 +16,7 @@ export class InitService {
|
|||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private stateService: StateServiceAbstraction,
|
||||
private twoFactorService: TwoFactorService,
|
||||
private logService: LogServiceAbstraction,
|
||||
private themingService: AbstractThemingService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
|
@ -24,6 +26,7 @@ export class InitService {
|
|||
return async () => {
|
||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
if (!BrowserPopupUtils.inPopup(window)) {
|
||||
window.document.body.classList.add("body-full");
|
||||
|
|
|
@ -15,10 +15,7 @@ import {
|
|||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||
|
@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d
|
|||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import {
|
||||
|
@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [
|
|||
useClass: UnauthGuardService,
|
||||
deps: [AuthServiceAbstraction, Router],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorService,
|
||||
useFactory: getBgService<TwoFactorService>("twoFactorService"),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthServiceAbstraction,
|
||||
useFactory: getBgService<AuthService>("authService"),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginStrategyServiceAbstraction,
|
||||
useFactory: getBgService<LoginStrategyServiceAbstraction>("loginStrategyService"),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SsoLoginServiceAbstraction,
|
||||
useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"),
|
||||
|
@ -487,7 +473,7 @@ const safeProviders: SafeProvider[] = [
|
|||
safeProvider({
|
||||
provide: DerivedStateProvider,
|
||||
useClass: ForegroundDerivedStateProvider,
|
||||
deps: [StorageServiceProvider, NgZone],
|
||||
deps: [NgZone],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutofillSettingsServiceAbstraction,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div bitDialogTitle>Bitwarden</div>
|
||||
<div bitDialogContent>
|
||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||
<p>{{ "version" | i18n }}: {{ version }}</p>
|
||||
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<p *ngIf="data.isCloud">
|
||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { combineLatest, map } from "rxjs";
|
||||
import { Observable, combineLatest, defer, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
@Component({
|
||||
templateUrl: "about.component.html",
|
||||
standalone: true,
|
||||
|
@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api";
|
|||
})
|
||||
export class AboutComponent {
|
||||
protected year = new Date().getFullYear();
|
||||
protected version = BrowserApi.getApplicationVersion();
|
||||
protected version$: Observable<string>;
|
||||
|
||||
protected data$ = combineLatest([
|
||||
this.configService.serverConfig$,
|
||||
|
@ -26,5 +25,8 @@ export class AboutComponent {
|
|||
constructor(
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
this.version$ = defer(() => this.platformUtilsService.getApplicationVersion());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface
|
|||
return;
|
||||
}
|
||||
|
||||
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag<boolean>(
|
||||
const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.BrowserFilelessImport,
|
||||
);
|
||||
const userAuthStatus = await this.authService.getAuthStatus();
|
||||
|
|
|
@ -46,7 +46,9 @@ export class BrowserSendStateService {
|
|||
* the send component on the browser
|
||||
*/
|
||||
async setBrowserSendComponentState(value: BrowserSendComponentState): Promise<void> {
|
||||
await this.activeUserBrowserSendComponentState.update(() => value);
|
||||
await this.activeUserBrowserSendComponentState.update(() => value, {
|
||||
shouldUpdate: (current) => !(current == null && value == null),
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the active user's browser component state
|
||||
|
@ -60,6 +62,8 @@ export class BrowserSendStateService {
|
|||
* @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser
|
||||
*/
|
||||
async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise<void> {
|
||||
await this.activeUserBrowserSendTypeComponentState.update(() => value);
|
||||
await this.activeUserBrowserSendTypeComponentState.update(() => value, {
|
||||
shouldUpdate: (current) => !(current == null && value == null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -292,6 +292,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
|||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
this.url,
|
||||
otherTypes.length > 0 ? otherTypes : null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
this.loginCiphers = [];
|
||||
|
|
|
@ -52,7 +52,9 @@ export class VaultBrowserStateService {
|
|||
}
|
||||
|
||||
async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise<void> {
|
||||
await this.activeUserVaultBrowserGroupingsComponentState.update(() => value);
|
||||
await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, {
|
||||
shouldUpdate: (current) => !(current == null && value == null),
|
||||
});
|
||||
}
|
||||
|
||||
async getBrowserVaultItemsComponentState(): Promise<BrowserComponentState> {
|
||||
|
@ -60,6 +62,8 @@ export class VaultBrowserStateService {
|
|||
}
|
||||
|
||||
async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise<void> {
|
||||
await this.activeUserVaultBrowserComponentState.update(() => value);
|
||||
await this.activeUserVaultBrowserComponentState.update(() => value, {
|
||||
shouldUpdate: (current) => !(current == null && value == null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "ES2020",
|
||||
|
|
|
@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
|||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
|
@ -68,6 +69,7 @@ export class LoginCommand {
|
|||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected orgService: OrganizationService,
|
||||
protected logoutCallback: () => Promise<void>,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
async run(email: string, password: string, options: OptionValues) {
|
||||
|
@ -229,7 +231,7 @@ export class LoginCommand {
|
|||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
}
|
||||
|
@ -270,7 +272,7 @@ export class LoginCommand {
|
|||
|
||||
if (
|
||||
twoFactorToken == null &&
|
||||
response.twoFactorProviders.size > 1 &&
|
||||
Object.keys(response.twoFactorProviders).length > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
|
@ -563,14 +565,12 @@ export class LoginCommand {
|
|||
message: "Master Password Hint (optional):",
|
||||
});
|
||||
const masterPasswordHint = hint.input;
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
|
||||
// Create new key and hash new password
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdf,
|
||||
kdfConfig,
|
||||
);
|
||||
const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey);
|
||||
|
|
|
@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
|
@ -34,6 +35,7 @@ export class UnlockCommand {
|
|||
private syncService: SyncService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private logout: () => Promise<void>,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
async run(password: string, cmdOptions: Record<string, any>) {
|
||||
|
@ -48,9 +50,8 @@ export class UnlockCommand {
|
|||
|
||||
await this.setNewSessionKey();
|
||||
const email = await this.stateService.getEmail();
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const storedMasterKeyHash = await firstValueFrom(
|
||||
this.masterPasswordService.masterKeyHash$(userId),
|
||||
|
|
|
@ -30,12 +30,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
|
@ -235,6 +237,7 @@ export class Main {
|
|||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||
providerApiService: ProviderApiServiceAbstraction;
|
||||
userKeyInitService: UserKeyInitService;
|
||||
kdfConfigService: KdfConfigServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
|
@ -311,7 +314,7 @@ export class Main {
|
|||
this.singleUserStateProvider,
|
||||
);
|
||||
|
||||
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
|
||||
this.derivedStateProvider = new DefaultDerivedStateProvider();
|
||||
|
||||
this.stateProvider = new DefaultStateProvider(
|
||||
this.activeUserStateProvider,
|
||||
|
@ -357,6 +360,8 @@ export class Main {
|
|||
|
||||
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
|
||||
|
||||
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
||||
|
||||
this.cryptoService = new CryptoService(
|
||||
this.masterPasswordService,
|
||||
this.keyGenerationService,
|
||||
|
@ -367,6 +372,7 @@ export class Main {
|
|||
this.stateService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||
|
@ -449,7 +455,11 @@ export class Main {
|
|||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
|
||||
this.twoFactorService = new TwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
|
@ -512,6 +522,7 @@ export class Main {
|
|||
this.userDecryptionOptionsService,
|
||||
this.globalStateProvider,
|
||||
this.billingAccountProfileStateService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.authService = new AuthService(
|
||||
|
@ -574,6 +585,7 @@ export class Main {
|
|||
this.cryptoService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.logService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.userVerificationService = new UserVerificationService(
|
||||
|
@ -588,6 +600,7 @@ export class Main {
|
|||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.platformUtilsService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.vaultTimeoutService = new VaultTimeoutService(
|
||||
|
@ -654,7 +667,7 @@ export class Main {
|
|||
this.cipherService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.stateService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.organizationExportService = new OrganizationVaultExportService(
|
||||
|
@ -662,8 +675,8 @@ export class Main {
|
|||
this.apiService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.stateService,
|
||||
this.collectionService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.exportService = new VaultExportService(
|
||||
|
|
|
@ -134,6 +134,7 @@ export class ServeCommand {
|
|||
this.main.syncService,
|
||||
this.main.organizationApiService,
|
||||
async () => await this.main.logout(),
|
||||
this.main.kdfConfigService,
|
||||
);
|
||||
|
||||
this.sendCreateCommand = new SendCreateCommand(
|
||||
|
|
|
@ -156,6 +156,7 @@ export class Program {
|
|||
this.main.policyApiService,
|
||||
this.main.organizationService,
|
||||
async () => await this.main.logout(),
|
||||
this.main.kdfConfigService,
|
||||
);
|
||||
const response = await command.run(email, password, options);
|
||||
this.processResponse(response, true);
|
||||
|
@ -265,6 +266,7 @@ export class Program {
|
|||
this.main.syncService,
|
||||
this.main.organizationApiService,
|
||||
async () => await this.main.logout(),
|
||||
this.main.kdfConfigService,
|
||||
);
|
||||
const response = await command.run(password, cmd);
|
||||
this.processResponse(response);
|
||||
|
@ -627,6 +629,7 @@ export class Program {
|
|||
this.main.syncService,
|
||||
this.main.organizationApiService,
|
||||
this.main.logout,
|
||||
this.main.kdfConfigService,
|
||||
);
|
||||
const response = await command.run(null, null);
|
||||
if (!response.success) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
|
@ -258,6 +259,7 @@ const safeProviders: SafeProvider[] = [
|
|||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
BiometricStateService,
|
||||
KdfConfigServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
|
|
@ -14,6 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
|
|||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
|
@ -164,6 +165,10 @@ describe("LockComponent", () => {
|
|||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: mock<KdfConfigService>(),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
|
|
@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
|
|||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
|
@ -63,6 +64,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
|
@ -87,6 +89,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac
|
|||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
|
@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
|||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
|
@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
|||
userDecryptionOptionsService,
|
||||
ssoLoginService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1636,7 +1636,7 @@
|
|||
"message": "Error enabling browser integration"
|
||||
},
|
||||
"browserIntegrationErrorDesc": {
|
||||
"message": "An error has occurred while enabling browser integration."
|
||||
"message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur."
|
||||
},
|
||||
"browserIntegrationMasOnlyDesc": {
|
||||
"message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment."
|
||||
|
@ -2698,7 +2698,7 @@
|
|||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"success": {
|
||||
"message": "Success"
|
||||
"message": "Succès"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"message": "Résolution de problèmes"
|
||||
|
|
|
@ -2698,7 +2698,7 @@
|
|||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"success": {
|
||||
"message": "Success"
|
||||
"message": "Sikeres"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"message": "Hibaelhárítás"
|
||||
|
|
|
@ -2698,7 +2698,7 @@
|
|||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"success": {
|
||||
"message": "Success"
|
||||
"message": "Izdevās"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"message": "Sarežģījumu novēršana"
|
||||
|
|
|
@ -2698,7 +2698,7 @@
|
|||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"success": {
|
||||
"message": "Success"
|
||||
"message": "Succes"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"message": "Probleemoplossing"
|
||||
|
|
|
@ -2698,7 +2698,7 @@
|
|||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"success": {
|
||||
"message": "Success"
|
||||
"message": "成功"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"message": "故障排除"
|
||||
|
|
|
@ -157,7 +157,7 @@ export class Main {
|
|||
activeUserStateProvider,
|
||||
singleUserStateProvider,
|
||||
globalStateProvider,
|
||||
new DefaultDerivedStateProvider(storageServiceProvider),
|
||||
new DefaultDerivedStateProvider(),
|
||||
);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(stateProvider, accountService);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
@ -35,6 +36,7 @@ describe("electronCryptoService", () => {
|
|||
let accountService: FakeAccountService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const kdfConfigService = mock<KdfConfigService>();
|
||||
|
||||
const mockUserId = "mock user id" as UserId;
|
||||
|
||||
|
@ -54,6 +56,7 @@ describe("electronCryptoService", () => {
|
|||
accountService,
|
||||
stateProvider,
|
||||
biometricStateService,
|
||||
kdfConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||
accountService: AccountService,
|
||||
stateProvider: StateProvider,
|
||||
private biometricStateService: BiometricStateService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
|
@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||
stateService,
|
||||
accountService,
|
||||
stateProvider,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
|
||||
<navigation-product-switcher></navigation-product-switcher>
|
||||
<navigation-product-switcher class="tw-mt-auto"></navigation-product-switcher>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
|
|
|
@ -60,7 +60,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||
|
||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -194,7 +194,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||
}),
|
||||
);
|
||||
}),
|
||||
shareReplay({ refCount: false }),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
restrictGroupAccess$ = combineLatest([
|
||||
|
|
|
@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy {
|
|||
groups: groups$,
|
||||
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
),
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
|
|
|
@ -7,10 +7,15 @@ import {
|
|||
OrganizationUserResetPasswordRequest,
|
||||
OrganizationUserResetPasswordWithIdRequest,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
@ -90,12 +95,17 @@ export class OrganizationUserResetPasswordService {
|
|||
const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey);
|
||||
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
|
||||
|
||||
// determine Kdf Algorithm
|
||||
const kdfConfig: KdfConfig =
|
||||
response.kdf === KdfType.PBKDF2_SHA256
|
||||
? new PBKDF2KdfConfig(response.kdfIterations)
|
||||
: new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism);
|
||||
|
||||
// Create new master key and hash new password
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
newMasterPassword,
|
||||
email.trim().toLowerCase(),
|
||||
response.kdf,
|
||||
new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism),
|
||||
kdfConfig,
|
||||
);
|
||||
const newMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
|
|
|
@ -44,12 +44,10 @@ export class AccountComponent {
|
|||
|
||||
protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsMigration,
|
||||
false,
|
||||
);
|
||||
|
||||
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
|
||||
// FormGroup validators taken from server Organization domain object
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
|||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
||||
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
||||
|
@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../..
|
|||
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||
tabbedHeader = false;
|
||||
constructor(
|
||||
dialogService: DialogService,
|
||||
apiService: ApiService,
|
||||
modalService: ModalService,
|
||||
messagingService: MessagingService,
|
||||
|
@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
|||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
super(
|
||||
dialogService,
|
||||
apiService,
|
||||
modalService,
|
||||
messagingService,
|
||||
|
|
|
@ -3,10 +3,15 @@ import { Injectable } from "@angular/core";
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
@ -231,16 +236,22 @@ export class EmergencyAccessService {
|
|||
|
||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
email,
|
||||
takeoverResponse.kdf,
|
||||
new KdfConfig(
|
||||
takeoverResponse.kdfIterations,
|
||||
takeoverResponse.kdfMemory,
|
||||
takeoverResponse.kdfParallelism,
|
||||
),
|
||||
);
|
||||
let config: KdfConfig;
|
||||
|
||||
switch (takeoverResponse.kdf) {
|
||||
case KdfType.PBKDF2_SHA256:
|
||||
config = new PBKDF2KdfConfig(takeoverResponse.kdfIterations);
|
||||
break;
|
||||
case KdfType.Argon2id:
|
||||
config = new Argon2KdfConfig(
|
||||
takeoverResponse.kdfIterations,
|
||||
takeoverResponse.kdfMemory,
|
||||
takeoverResponse.kdfParallelism,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const masterKey = await this.cryptoService.makeMasterKey(masterPassword, email, config);
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
|
||||
|
||||
const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -47,6 +48,7 @@ describe("KeyRotationService", () => {
|
|||
let mockEncryptService: MockProxy<EncryptService>;
|
||||
let mockStateService: MockProxy<StateService>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockKdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
@ -65,6 +67,7 @@ describe("KeyRotationService", () => {
|
|||
mockEncryptService = mock<EncryptService>();
|
||||
mockStateService = mock<StateService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockKdfConfigService = mock<KdfConfigService>();
|
||||
|
||||
keyRotationService = new UserKeyRotationService(
|
||||
mockMasterPasswordService,
|
||||
|
@ -80,6 +83,7 @@ describe("KeyRotationService", () => {
|
|||
mockStateService,
|
||||
mockAccountService,
|
||||
mockConfigService,
|
||||
mockKdfConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
|
|||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
@ -39,6 +40,7 @@ export class UserKeyRotationService {
|
|||
private stateService: StateService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
@ -54,8 +56,7 @@ export class UserKeyRotationService {
|
|||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig(),
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
);
|
||||
|
||||
if (!masterKey) {
|
||||
|
@ -89,7 +90,7 @@ export class UserKeyRotationService {
|
|||
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
||||
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
||||
|
||||
if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
|
||||
await this.apiService.postUserKeyUpdate(request);
|
||||
} else {
|
||||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
|
@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit {
|
|||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -83,12 +85,10 @@ export class ChangeEmailComponent implements OnInit {
|
|||
step1Value.masterPassword,
|
||||
await this.cryptoService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
step1Value.masterPassword,
|
||||
newEmail,
|
||||
kdf,
|
||||
kdfConfig,
|
||||
);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -48,6 +49,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||
dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private keyRotationService: UserKeyRotationService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -58,6 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||
policyService,
|
||||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { takeUntil } from "rxjs";
|
|||
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
@ -58,6 +59,7 @@ export class EmergencyAccessTakeoverComponent
|
|||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -68,6 +70,7 @@ export class EmergencyAccessTakeoverComponent
|
|||
policyService,
|
||||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Component, Inject } from "@angular/core";
|
|||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -18,7 +19,6 @@ import { KdfType } from "@bitwarden/common/platform/enums";
|
|||
templateUrl: "change-kdf-confirmation.component.html",
|
||||
})
|
||||
export class ChangeKdfConfirmationComponent {
|
||||
kdf: KdfType;
|
||||
kdfConfig: KdfConfig;
|
||||
|
||||
form = new FormGroup({
|
||||
|
@ -37,9 +37,9 @@ export class ChangeKdfConfirmationComponent {
|
|||
private messagingService: MessagingService,
|
||||
private stateService: StateService,
|
||||
private logService: LogService,
|
||||
@Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig },
|
||||
private kdfConfigService: KdfConfigService,
|
||||
@Inject(DIALOG_DATA) params: { kdfConfig: KdfConfig },
|
||||
) {
|
||||
this.kdf = params.kdf;
|
||||
this.kdfConfig = params.kdfConfig;
|
||||
this.masterPassword = null;
|
||||
}
|
||||
|
@ -65,22 +65,24 @@ export class ChangeKdfConfirmationComponent {
|
|||
|
||||
private async makeKeyAndSaveAsync() {
|
||||
const masterPassword = this.form.value.masterPassword;
|
||||
|
||||
// Ensure the KDF config is valid.
|
||||
this.kdfConfig.validateKdfConfig();
|
||||
|
||||
const request = new KdfRequest();
|
||||
request.kdf = this.kdf;
|
||||
request.kdf = this.kdfConfig.kdfType;
|
||||
request.kdfIterations = this.kdfConfig.iterations;
|
||||
request.kdfMemory = this.kdfConfig.memory;
|
||||
request.kdfParallelism = this.kdfConfig.parallelism;
|
||||
if (this.kdfConfig.kdfType === KdfType.Argon2id) {
|
||||
request.kdfMemory = this.kdfConfig.memory;
|
||||
request.kdfParallelism = this.kdfConfig.parallelism;
|
||||
}
|
||||
const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword);
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey);
|
||||
const email = await this.stateService.getEmail();
|
||||
|
||||
// Ensure the KDF config is valid.
|
||||
this.cryptoService.validateKdfConfig(this.kdf, this.kdfConfig);
|
||||
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
masterPassword,
|
||||
email,
|
||||
this.kdf,
|
||||
this.kdfConfig,
|
||||
);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
|
|
|
@ -19,14 +19,14 @@
|
|||
<select
|
||||
id="kdf"
|
||||
name="Kdf"
|
||||
[(ngModel)]="kdf"
|
||||
[(ngModel)]="kdfConfig.kdfType"
|
||||
(ngModelChange)="onChangeKdf($event)"
|
||||
class="form-control mb-3"
|
||||
required
|
||||
>
|
||||
<option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<ng-container *ngIf="isArgon2(kdfConfig)">
|
||||
<label for="kdfMemory">{{ "kdfMemory" | i18n }}</label>
|
||||
<input
|
||||
id="kdfMemory"
|
||||
|
@ -43,7 +43,7 @@
|
|||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group mb-0">
|
||||
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
|
||||
<ng-container *ngIf="isPBKDF2(kdfConfig)">
|
||||
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
|
@ -65,7 +65,7 @@
|
|||
required
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<ng-container *ngIf="isArgon2(kdfConfig)">
|
||||
<label for="kdfIterations">{{ "kdfIterations" | i18n }}</label>
|
||||
<input
|
||||
id="iterations"
|
||||
|
@ -92,7 +92,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256">
|
||||
<ng-container *ngIf="isPBKDF2(kdfConfig)">
|
||||
<p class="small form-text text-muted">
|
||||
{{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }}
|
||||
</p>
|
||||
|
@ -100,7 +100,7 @@
|
|||
{{ "kdfIterationsWarning" | i18n: (100000 | number) }}
|
||||
</bit-callout>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="kdf == kdfType.Argon2id">
|
||||
<ng-container *ngIf="isArgon2(kdfConfig)">
|
||||
<p class="small form-text text-muted">{{ "argon2Desc" | i18n }}</p>
|
||||
<bit-callout type="warning"> {{ "argon2Warning" | i18n }}</bit-callout>
|
||||
</ng-container>
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
PBKDF2_ITERATIONS,
|
||||
|
@ -19,7 +23,6 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon
|
|||
templateUrl: "change-kdf.component.html",
|
||||
})
|
||||
export class ChangeKdfComponent implements OnInit {
|
||||
kdf = KdfType.PBKDF2_SHA256;
|
||||
kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG;
|
||||
kdfType = KdfType;
|
||||
kdfOptions: any[] = [];
|
||||
|
@ -31,8 +34,8 @@ export class ChangeKdfComponent implements OnInit {
|
|||
protected ARGON2_PARALLELISM = ARGON2_PARALLELISM;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private dialogService: DialogService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
this.kdfOptions = [
|
||||
{ name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 },
|
||||
|
@ -41,19 +44,22 @@ export class ChangeKdfComponent implements OnInit {
|
|||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.kdf = await this.stateService.getKdfType();
|
||||
this.kdfConfig = await this.stateService.getKdfConfig();
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
}
|
||||
|
||||
isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig {
|
||||
return t instanceof PBKDF2KdfConfig;
|
||||
}
|
||||
|
||||
isArgon2(t: KdfConfig): t is Argon2KdfConfig {
|
||||
return t instanceof Argon2KdfConfig;
|
||||
}
|
||||
|
||||
async onChangeKdf(newValue: KdfType) {
|
||||
if (newValue === KdfType.PBKDF2_SHA256) {
|
||||
this.kdfConfig = new KdfConfig(PBKDF2_ITERATIONS.defaultValue);
|
||||
this.kdfConfig = new PBKDF2KdfConfig();
|
||||
} else if (newValue === KdfType.Argon2id) {
|
||||
this.kdfConfig = new KdfConfig(
|
||||
ARGON2_ITERATIONS.defaultValue,
|
||||
ARGON2_MEMORY.defaultValue,
|
||||
ARGON2_PARALLELISM.defaultValue,
|
||||
);
|
||||
this.kdfConfig = new Argon2KdfConfig();
|
||||
} else {
|
||||
throw new Error("Unknown KDF type.");
|
||||
}
|
||||
|
@ -62,7 +68,6 @@ export class ChangeKdfComponent implements OnInit {
|
|||
async openConfirmationModal() {
|
||||
this.dialogService.open(ChangeKdfConfirmationComponent, {
|
||||
data: {
|
||||
kdf: this.kdf,
|
||||
kdfConfig: this.kdfConfig,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed">
|
||||
</app-two-factor-verify>
|
||||
<ng-container *ngIf="authed">
|
||||
<div class="modal-body text-center">
|
||||
<ng-container *ngIf="code">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
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 { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
|
||||
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
|
||||
import { TwoFactorEmailComponent } from "./two-factor-email.component";
|
||||
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
|
||||
import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
|
||||
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
|
||||
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
|
||||
|
||||
|
@ -52,6 +60,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
|||
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
|
||||
|
||||
constructor(
|
||||
protected dialogService: DialogService,
|
||||
protected apiService: ApiService,
|
||||
protected modalService: ModalService,
|
||||
protected messagingService: MessagingService,
|
||||
|
@ -114,50 +123,82 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
|||
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) {
|
||||
switch (type) {
|
||||
case TwoFactorProviderType.Authenticator: {
|
||||
const result: AuthResponse<TwoFactorAuthenticatorResponse> =
|
||||
await this.callTwoFactorVerifyDialog(type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const authComp = await this.openModal(
|
||||
this.authenticatorModalRef,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
authComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
await authComp.auth(result);
|
||||
authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Authenticator);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Yubikey: {
|
||||
const result: AuthResponse<TwoFactorYubiKeyResponse> =
|
||||
await this.callTwoFactorVerifyDialog(type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
yubiComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
yubiComp.auth(result);
|
||||
yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Yubikey);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Duo: {
|
||||
const result: AuthResponse<TwoFactorDuoResponse> =
|
||||
await this.callTwoFactorVerifyDialog(type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
duoComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
duoComp.auth(result);
|
||||
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Duo);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.Email: {
|
||||
const result: AuthResponse<TwoFactorEmailResponse> =
|
||||
await this.callTwoFactorVerifyDialog(type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
emailComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
await emailComp.auth(result);
|
||||
emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.WebAuthn: {
|
||||
const result: AuthResponse<TwoFactorWebAuthnResponse> =
|
||||
await this.callTwoFactorVerifyDialog(type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const webAuthnComp = await this.openModal(
|
||||
this.webAuthnModalRef,
|
||||
TwoFactorWebAuthnComponent,
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
webAuthnComp.onUpdated.subscribe((enabled: boolean) => {
|
||||
webAuthnComp.auth(result);
|
||||
webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
|
||||
});
|
||||
break;
|
||||
|
@ -167,10 +208,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
recoveryCode() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
|
||||
async recoveryCode() {
|
||||
const result = await this.callTwoFactorVerifyDialog(-1 as TwoFactorProviderType);
|
||||
if (result) {
|
||||
const recoverComp = await this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
|
||||
recoverComp.auth(result);
|
||||
}
|
||||
}
|
||||
|
||||
async premiumRequired() {
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-body">
|
||||
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "continue" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default">
|
||||
<span bitDialogTitle>
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small class="tw-text-muted">{{ dialogTitle }}</small>
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<app-user-verification-form-input
|
||||
formControlName="secret"
|
||||
ngDefaultControl
|
||||
name="secret"
|
||||
></app-user-verification-form-input>
|
||||
</ng-container>
|
||||
<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>
|
||||
|
|
|
@ -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 { 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 { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
||||
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({
|
||||
selector: "app-two-factor-verify",
|
||||
templateUrl: "two-factor-verify.component.html",
|
||||
})
|
||||
export class TwoFactorVerifyComponent {
|
||||
@Input() type: TwoFactorProviderType;
|
||||
@Input() organizationId: string;
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
||||
|
||||
secret: Verification;
|
||||
formPromise: Promise<TwoFactorResponse>;
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
secret: new FormControl<Verification | null>(null),
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
|
||||
private dialogRef: DialogRef,
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {}
|
||||
) {
|
||||
this.type = data.type;
|
||||
this.organizationId = data.organizationId;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
submit = async () => {
|
||||
let hashedSecret: string;
|
||||
|
||||
try {
|
||||
this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.formGroup.value.secret)
|
||||
.then((request) => {
|
||||
hashedSecret =
|
||||
this.secret.type === VerificationType.MasterPassword
|
||||
this.formGroup.value.secret.type === VerificationType.MasterPassword
|
||||
? request.masterPasswordHash
|
||||
: request.otp;
|
||||
return this.apiCall(request);
|
||||
});
|
||||
|
||||
const response = await this.formPromise;
|
||||
this.onAuthed.emit({
|
||||
response: response,
|
||||
secret: hashedSecret,
|
||||
verificationType: this.secret.type,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
const response = await this.formPromise;
|
||||
this.dialogRef.close({
|
||||
response: response,
|
||||
secret: hashedSecret,
|
||||
verificationType: this.formGroup.value.secret.type,
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) {
|
||||
return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<app-two-factor-verify
|
||||
[organizationId]="organizationId"
|
||||
[type]="type"
|
||||
(onAuthed)="auth($any($event))"
|
||||
*ngIf="!authed"
|
||||
>
|
||||
</app-two-factor-verify>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Router } from "@angular/router";
|
|||
import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -32,6 +33,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
|||
stateService: StateService,
|
||||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
|
@ -46,6 +48,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
|||
userVerificationService,
|
||||
logService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||
|
||||
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,25 +13,27 @@
|
|||
</span>
|
||||
</ng-container>
|
||||
</bit-nav-item>
|
||||
<section
|
||||
*ngIf="((moreProducts$ | async) ?? []).length > 0"
|
||||
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-alt2"
|
||||
>
|
||||
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
|
||||
<a
|
||||
*ngFor="let more of moreProducts$ | async"
|
||||
[href]="more.marketingRoute"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
|
||||
<ng-container *ngIf="(moreProducts$ | async) ?? [] as moreProducts">
|
||||
<section
|
||||
*ngIf="moreProducts.length > 0"
|
||||
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-alt2"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1"></i>
|
||||
<div>
|
||||
{{ more.navigationUIDetails?.name ?? more.name }}
|
||||
<div *ngIf="more.navigationUIDetails?.supportingText" class="tw-text-xs tw-font-normal">
|
||||
{{ more.navigationUIDetails.supportingText }}
|
||||
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
|
||||
<a
|
||||
*ngFor="let more of moreProducts"
|
||||
[href]="more.marketingRoute"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1"></i>
|
||||
<div>
|
||||
{{ more.otherProductOverrides?.name ?? more.name }}
|
||||
<div *ngIf="more.otherProductOverrides?.supportingText" class="tw-text-xs tw-font-normal">
|
||||
{{ more.otherProductOverrides.supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
</a>
|
||||
</section>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
@ -77,7 +77,7 @@ describe("NavigationProductSwitcherComponent", () => {
|
|||
expect(link.getAttribute("href")).toBe("https://www.example.com/");
|
||||
});
|
||||
|
||||
it("uses `navigationUIDetails` when available", () => {
|
||||
it("uses `otherProductOverrides` when available", () => {
|
||||
mockProducts$.next({
|
||||
bento: [],
|
||||
other: [
|
||||
|
@ -86,7 +86,7 @@ describe("NavigationProductSwitcherComponent", () => {
|
|||
name: "Other Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
navigationUIDetails: { name: "Alternate name" },
|
||||
otherProductOverrides: { name: "Alternate name" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -103,7 +103,7 @@ describe("NavigationProductSwitcherComponent", () => {
|
|||
name: "Other Product",
|
||||
icon: "bwi-lock",
|
||||
marketingRoute: "https://www.example.com/",
|
||||
navigationUIDetails: { name: "Alternate name", supportingText: "Supporting Text" },
|
||||
otherProductOverrides: { name: "Alternate name", supportingText: "Supporting Text" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, HostBinding } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service";
|
||||
|
@ -8,17 +8,15 @@ import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-s
|
|||
templateUrl: "./navigation-switcher.component.html",
|
||||
})
|
||||
export class NavigationProductSwitcherComponent {
|
||||
// Use margin-top: auto to push the component to the bottom of the parent flex container
|
||||
@HostBinding("style.margin-top") marginTop = "auto";
|
||||
|
||||
constructor(private productSwitcherService: ProductSwitcherService) {}
|
||||
|
||||
accessibleProducts$: Observable<ProductSwitcherItem[]> =
|
||||
protected readonly accessibleProducts$: Observable<ProductSwitcherItem[]> =
|
||||
this.productSwitcherService.products$.pipe(
|
||||
map((products) => (products.bento ?? []).filter((item) => !item.isActive)),
|
||||
);
|
||||
|
||||
moreProducts$: Observable<ProductSwitcherItem[]> = this.productSwitcherService.products$.pipe(
|
||||
map((products) => (products.other ?? []).filter((item) => !item.isActive)),
|
||||
);
|
||||
protected readonly moreProducts$: Observable<ProductSwitcherItem[]> =
|
||||
this.productSwitcherService.products$.pipe(
|
||||
map((products) => (products.other ?? []).filter((item) => !item.isActive)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryFn } from "@storybook/angular";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
|
@ -115,40 +115,58 @@ export default {
|
|||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
} as Meta<NavigationProductSwitcherComponent>;
|
||||
|
||||
const Template: StoryFn = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
type Story = StoryObj<
|
||||
NavigationProductSwitcherComponent & MockProviderService & MockOrganizationService
|
||||
>;
|
||||
|
||||
const Template: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet>
|
||||
<bit-layout>
|
||||
<nav slot="sidebar" class="tw-flex tw-flex-col tw-h-full">
|
||||
<navigation-product-switcher></navigation-product-switcher>
|
||||
</nav>
|
||||
</bit-layout>
|
||||
`,
|
||||
});
|
||||
|
||||
export const OnlyPM = Template.bind({});
|
||||
OnlyPM.args = {
|
||||
mockOrgs: [],
|
||||
mockProviders: [],
|
||||
<div class="tw-bg-background-alt3 tw-w-60">
|
||||
<navigation-product-switcher></navigation-product-switcher>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SMAvailable = Template.bind({});
|
||||
SMAvailable.args = {
|
||||
mockOrgs: [{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true }],
|
||||
mockProviders: [],
|
||||
export const OnlyPM: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
mockOrgs: [],
|
||||
mockProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const SMAndACAvailable = Template.bind({});
|
||||
SMAndACAvailable.args = {
|
||||
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
|
||||
mockProviders: [],
|
||||
export const SMAvailable: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
mockOrgs: [
|
||||
{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true },
|
||||
] as Organization[],
|
||||
mockProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAllOptions = Template.bind({});
|
||||
WithAllOptions.args = {
|
||||
mockOrgs: [{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true }],
|
||||
mockProviders: [{ id: "provider-a" }],
|
||||
export const SMAndACAvailable: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
mockOrgs: [
|
||||
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
|
||||
] as Organization[],
|
||||
mockProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAllOptions: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
mockOrgs: [
|
||||
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
|
||||
] as Organization[],
|
||||
mockProviders: [{ id: "provider-a" }] as Provider[],
|
||||
},
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue