1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 22:33:35 +00:00

Merge branch 'main' into auth/pm-17185/remove-bootstrap-from-SsoComponent

This commit is contained in:
Alec Rippberger
2025-02-19 10:47:30 -06:00
committed by GitHub
73 changed files with 1232 additions and 581 deletions

9
.github/CODEOWNERS vendored
View File

@@ -4,6 +4,11 @@
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
## Desktop native module ##
apps/desktop/desktop_native @bitwarden/team-platform-dev
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
## Auth team files ##
apps/browser/src/auth @bitwarden/team-auth-dev
apps/cli/src/auth @bitwarden/team-auth-dev
@@ -124,10 +129,6 @@ apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation
apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation
apps/web/src/app/layouts @bitwarden/team-ui-foundation
## Desktop native module ##
apps/desktop/desktop_native @bitwarden/team-platform-dev
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
## Key management team files ##
apps/desktop/src/key-management @bitwarden/team-key-management-dev

View File

@@ -1,46 +1,46 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>bitwarden/renovate-config"],
"enabledManagers": ["cargo", "github-actions", "npm"],
"packageRules": [
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: ["github>bitwarden/renovate-config"], // Extends our base config for pinned dependencies
enabledManagers: ["cargo", "github-actions", "npm"],
packageRules: [
{
"groupName": "github-action minor",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor"]
groupName: "github-action minor",
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor"],
},
{
"matchManagers": ["cargo"],
"commitMessagePrefix": "[deps] Platform:"
matchManagers: ["cargo"],
commitMessagePrefix: "[deps] Platform:",
},
{
"groupName": "napi",
"matchPackageNames": ["napi", "napi-build", "napi-derive"]
groupName: "napi",
matchPackageNames: ["napi", "napi-build", "napi-derive"],
},
{
"matchPackageNames": ["typescript", "zone.js"],
"matchUpdateTypes": ["major", "minor"],
"description": "Determined by Angular",
"enabled": false
matchPackageNames: ["typescript", "zone.js"],
matchUpdateTypes: ["major", "minor"],
description: "Determined by Angular",
enabled: false,
},
{
"matchPackageNames": ["typescript", "zone.js"],
"matchUpdateTypes": "patch"
matchPackageNames: ["typescript", "zone.js"],
matchUpdateTypes: "patch",
},
{
"groupName": "jest",
"matchPackageNames": ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
"matchUpdateTypes": "major"
groupName: "jest",
matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
matchUpdateTypes: "major",
},
{
"groupName": "macOS/iOS bindings",
"matchPackageNames": ["core-foundation", "security-framework", "security-framework-sys"]
groupName: "macOS/iOS bindings",
matchPackageNames: ["core-foundation", "security-framework", "security-framework-sys"],
},
{
"groupName": "zbus",
"matchPackageNames": ["zbus", "zbus_polkit"]
groupName: "zbus",
matchPackageNames: ["zbus", "zbus_polkit"],
},
{
"matchPackageNames": [
matchPackageNames: [
"base64-loader",
"buffer",
"bufferutil",
@@ -56,20 +56,20 @@
"style-loader",
"ts-loader",
"url",
"util"
"util",
],
"description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"]
description: "Admin Console owned dependencies",
commitMessagePrefix: "[deps] AC:",
reviewers: ["team:team-admin-console-dev"],
},
{
"matchPackageNames": ["qrious"],
"description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
matchPackageNames: ["qrious"],
description: "Auth owned dependencies",
commitMessagePrefix: "[deps] Auth:",
reviewers: ["team:team-auth-dev"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@angular-eslint/schematics",
"angular-eslint",
"eslint-config-prettier",
@@ -82,14 +82,14 @@
"eslint",
"husky",
"lint-staged",
"typescript-eslint"
"typescript-eslint",
],
"description": "Architecture owned dependencies",
"commitMessagePrefix": "[deps] Architecture:",
"reviewers": ["team:dept-architecture"]
description: "Architecture owned dependencies",
commitMessagePrefix: "[deps] Architecture:",
reviewers: ["team:dept-architecture"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@angular-eslint/eslint-plugin-template",
"@angular-eslint/eslint-plugin",
"@angular-eslint/schematics",
@@ -105,13 +105,13 @@
"eslint-plugin-tailwindcss",
"eslint",
"husky",
"lint-staged"
"lint-staged",
],
"groupName": "Linting minor-patch",
"matchUpdateTypes": ["minor", "patch"]
groupName: "Linting minor-patch",
matchUpdateTypes: ["minor", "patch"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@emotion/css",
"@webcomponents/custom-elements",
"concurrently",
@@ -126,20 +126,20 @@
"@storybook/web-components-webpack5",
"tabbable",
"tldts",
"wait-on"
"wait-on",
],
"description": "Autofill owned dependencies",
"commitMessagePrefix": "[deps] Autofill:",
"reviewers": ["team:team-autofill-dev"]
description: "Autofill owned dependencies",
commitMessagePrefix: "[deps] Autofill:",
reviewers: ["team:team-autofill-dev"],
},
{
"matchPackageNames": ["braintree-web-drop-in"],
"description": "Billing owned dependencies",
"commitMessagePrefix": "[deps] Billing:",
"reviewers": ["team:team-billing-dev"]
matchPackageNames: ["braintree-web-drop-in"],
description: "Billing owned dependencies",
commitMessagePrefix: "[deps] Billing:",
reviewers: ["team:team-billing-dev"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@babel/core",
"@babel/preset-env",
"@bitwarden/sdk-internal",
@@ -167,6 +167,7 @@
"electron-updater",
"html-webpack-injector",
"html-webpack-plugin",
"json5",
"lowdb",
"node-forge",
"node-ipc",
@@ -179,14 +180,14 @@
"webpack",
"webpack-cli",
"webpack-dev-server",
"webpack-node-externals"
"webpack-node-externals",
],
"description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:",
"reviewers": ["team:team-platform-dev"]
description: "Platform owned dependencies",
commitMessagePrefix: "[deps] Platform:",
reviewers: ["team:team-platform-dev"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@angular-devkit/build-angular",
"@angular/animations",
"@angular/cdk",
@@ -208,6 +209,7 @@
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-links",
"@storybook/addon-themes",
"@storybook/angular",
"@storybook/manager-api",
"@storybook/theming",
@@ -225,27 +227,27 @@
"remark-gfm",
"storybook",
"tailwindcss",
"zone.js"
"zone.js",
],
"description": "UI Foundation owned dependencies",
"commitMessagePrefix": "[deps] UI Foundation:",
"reviewers": ["team:team-ui-foundation"]
description: "UI Foundation owned dependencies",
commitMessagePrefix: "[deps] UI Foundation:",
reviewers: ["team:team-ui-foundation"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@types/jest",
"jest-junit",
"jest-mock-extended",
"jest-preset-angular",
"jest-diff",
"ts-jest"
"ts-jest",
],
"description": "Secrets Manager owned dependencies",
"commitMessagePrefix": "[deps] SM:",
"reviewers": ["team:team-secrets-manager-dev"]
description: "Secrets Manager owned dependencies",
commitMessagePrefix: "[deps] SM:",
reviewers: ["team:team-secrets-manager-dev"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@microsoft/signalr-protocol-msgpack",
"@microsoft/signalr",
"@types/jsdom",
@@ -256,14 +258,14 @@
"oidc-client-ts",
"papaparse",
"utf-8-validate",
"zxcvbn"
"zxcvbn",
],
"description": "Tools owned dependencies",
"commitMessagePrefix": "[deps] Tools:",
"reviewers": ["team:team-tools-dev"]
description: "Tools owned dependencies",
commitMessagePrefix: "[deps] Tools:",
reviewers: ["team:team-tools-dev"],
},
{
"matchPackageNames": [
matchPackageNames: [
"@koa/multer",
"@koa/router",
"@types/inquirer",
@@ -289,18 +291,18 @@
"node-fetch",
"open",
"proper-lockfile",
"qrcode-parser"
"qrcode-parser",
],
"description": "Vault owned dependencies",
"commitMessagePrefix": "[deps] Vault:",
"reviewers": ["team:team-vault-dev"]
description: "Vault owned dependencies",
commitMessagePrefix: "[deps] Vault:",
reviewers: ["team:team-vault-dev"],
},
{
"matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
"description": "Key Management owned dependencies",
"commitMessagePrefix": "[deps] KM:",
"reviewers": ["team:team-key-management-dev"]
}
matchPackageNames: ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
description: "Key Management owned dependencies",
commitMessagePrefix: "[deps] KM:",
reviewers: ["team:team-key-management-dev"],
},
],
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]
ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"],
}

View File

@@ -29,6 +29,7 @@ const config: StorybookConfig = {
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-designs"),
getAbsolutePath("@storybook/addon-interactions"),
getAbsolutePath("@storybook/addon-themes"),
{
// @storybook/addon-docs is part of @storybook/addon-essentials
// eslint-disable-next-line storybook/no-uninstalled-addons

View File

@@ -1,60 +1,30 @@
import { setCompodocJson } from "@storybook/addon-docs/angular";
import { withThemeByClassName } from "@storybook/addon-themes";
import { componentWrapperDecorator } from "@storybook/angular";
import type { Preview } from "@storybook/angular";
import docJson from "../documentation.json";
setCompodocJson(docJson);
const decorator = componentWrapperDecorator(
(story) => {
return /*html*/ `
<div
class="tw-border-2 tw-border-solid tw-px-5 tw-py-10"
[ngClass]="{
'tw-bg-[#ffffff] tw-border-secondary-300': theme === 'light',
'tw-bg-[#1f242e]': theme === 'dark',
}"
>
${story}
</div>
const wrapperDecorator = componentWrapperDecorator((story) => {
return /*html*/ `
<div class="tw-bg-background tw-px-5 tw-py-10">
${story}
</div>
`;
},
({ globals }) => {
// We need to add the theme class to the body to support body-appended content like popovers and menus
document.body.classList.remove("theme_light");
document.body.classList.remove("theme_dark");
document.body.classList.add(`theme_${globals["theme"]}`);
return { theme: `${globals["theme"]}` };
},
);
});
const preview: Preview = {
decorators: [decorator],
globalTypes: {
theme: {
description: "Global theme for components",
defaultValue: "light",
toolbar: {
title: "Theme",
icon: "circlehollow",
items: [
{
title: "Light",
value: "light",
icon: "sun",
},
{
title: "Dark",
value: "dark",
icon: "moon",
},
],
dynamicTitle: true,
decorators: [
withThemeByClassName({
themes: {
light: "theme_light",
dark: "theme_dark",
},
},
},
defaultTheme: "light",
}),
wrapperDecorator,
],
parameters: {
controls: {
matchers: {
@@ -69,6 +39,9 @@ const preview: Preview = {
},
},
docs: { source: { type: "dynamic", excludeDecorators: true } },
backgrounds: {
disable: true,
},
},
tags: ["autodocs"],
};

View File

@@ -1,5 +1,6 @@
type InitContextMenuItems = Omit<chrome.contextMenus.CreateProperties, "contexts"> & {
checkPremiumAccess?: boolean;
requiresPremiumAccess?: boolean;
requiresUnblockedUri?: boolean;
};
export { InitContextMenuItems };

View File

@@ -21,7 +21,7 @@ export class CipherContextMenuHandler {
private accountService: AccountService,
) {}
async update(url: string) {
async update(url: string, currentUriIsBlocked: boolean = false) {
if (this.mainContextMenuHandler.initRunning) {
return;
}
@@ -88,6 +88,10 @@ export class CipherContextMenuHandler {
for (const cipher of ciphers) {
await this.updateForCipher(cipher);
}
if (currentUriIsBlocked) {
await this.mainContextMenuHandler.removeBlockedUriMenuItems();
}
}
private async updateForCipher(cipher: CipherView) {

View File

@@ -2,7 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
import {
AUTOFILL_CARD_ID,
AUTOFILL_ID,
AUTOFILL_IDENTITY_ID,
COPY_IDENTIFIER_ID,
COPY_PASSWORD_ID,
COPY_USERNAME_ID,
COPY_VERIFICATION_CODE_ID,
NOOP_COMMAND_SUFFIX,
SEPARATOR_ID,
} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -15,6 +25,43 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { MainContextMenuHandler } from "./main-context-menu-handler";
/**
* Used in place of Set method `symmetricDifference`, which is only available to node version 22.0.0 or greater:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
*/
function symmetricDifference(setA: Set<string>, setB: Set<string>) {
const _difference = new Set(setA);
for (const elem of setB) {
if (_difference.has(elem)) {
_difference.delete(elem);
} else {
_difference.add(elem);
}
}
return _difference;
}
const createCipher = (data?: {
id?: CipherView["id"];
username?: CipherView["login"]["username"];
password?: CipherView["login"]["password"];
totp?: CipherView["login"]["totp"];
viewPassword?: CipherView["viewPassword"];
}): CipherView => {
const { id, username, password, totp, viewPassword } = data || {};
const cipherView = new CipherView(
new Cipher({
id: id ?? "1",
type: CipherType.Login,
viewPassword: viewPassword ?? true,
} as any),
);
cipherView.login.username = username ?? "USERNAME";
cipherView.login.password = password ?? "PASSWORD";
cipherView.login.totp = totp ?? "TOTP";
return cipherView;
};
describe("context-menu", () => {
let stateService: MockProxy<StateService>;
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
@@ -59,6 +106,9 @@ describe("context-menu", () => {
billingAccountProfileStateService,
accountService,
);
jest.spyOn(MainContextMenuHandler, "remove");
autofillSettingsService.enableContextMenu$ = of(true);
accountService.activeAccount$ = of({
id: "userId" as UserId,
@@ -68,7 +118,10 @@ describe("context-menu", () => {
});
});
afterEach(() => jest.resetAllMocks());
afterEach(async () => {
await MainContextMenuHandler.removeAll();
jest.resetAllMocks();
});
describe("init", () => {
it("has menu disabled", async () => {
@@ -97,27 +150,6 @@ describe("context-menu", () => {
});
describe("loadOptions", () => {
const createCipher = (data?: {
id?: CipherView["id"];
username?: CipherView["login"]["username"];
password?: CipherView["login"]["password"];
totp?: CipherView["login"]["totp"];
viewPassword?: CipherView["viewPassword"];
}): CipherView => {
const { id, username, password, totp, viewPassword } = data || {};
const cipherView = new CipherView(
new Cipher({
id: id ?? "1",
type: CipherType.Login,
viewPassword: viewPassword ?? true,
} as any),
);
cipherView.login.username = username ?? "USERNAME";
cipherView.login.password = password ?? "PASSWORD";
cipherView.login.totp = totp ?? "TOTP";
return cipherView;
};
it("is not a login cipher", async () => {
await sut.loadOptions("TEST_TITLE", "1", {
...createCipher(),
@@ -128,33 +160,124 @@ describe("context-menu", () => {
});
it("creates item for autofill", async () => {
await sut.loadOptions(
"TEST_TITLE",
"1",
createCipher({
username: "",
totp: "",
viewPassword: false,
}),
const cipher = createCipher({
username: "",
totp: "",
viewPassword: true,
});
const optionId = "1";
await sut.loadOptions("TEST_TITLE", optionId, cipher);
expect(createSpy).toHaveBeenCalledTimes(2);
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(2);
const expectedMenuItems = new Set([
AUTOFILL_ID + `_${optionId}`,
COPY_PASSWORD_ID + `_${optionId}`,
]);
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
const expectedReceivedDiff = symmetricDifference(
expectedMenuItems,
MainContextMenuHandler["existingMenuItems"],
);
expect(createSpy).toHaveBeenCalledTimes(1);
expect(expectedReceivedDiff.size).toEqual(0);
});
it("create entry for each cipher piece", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
await sut.loadOptions("TEST_TITLE", "1", createCipher());
const optionId = "arbitraryString";
await sut.loadOptions("TEST_TITLE", optionId, createCipher());
expect(createSpy).toHaveBeenCalledTimes(4);
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
const expectedMenuItems = new Set([
AUTOFILL_ID + `_${optionId}`,
COPY_PASSWORD_ID + `_${optionId}`,
COPY_USERNAME_ID + `_${optionId}`,
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
]);
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
const expectedReceivedDiff = symmetricDifference(
expectedMenuItems,
MainContextMenuHandler["existingMenuItems"],
);
expect(expectedReceivedDiff.size).toEqual(0);
});
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
await sut.loadOptions("TEST_TITLE", "NOOP");
const optionId = "NOOP";
await sut.loadOptions("TEST_TITLE", optionId);
expect(createSpy).toHaveBeenCalledTimes(6);
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(6);
const expectedMenuItems = new Set([
AUTOFILL_ID + `_${optionId}`,
COPY_PASSWORD_ID + `_${optionId}`,
COPY_USERNAME_ID + `_${optionId}`,
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
AUTOFILL_CARD_ID + `_${optionId}`,
AUTOFILL_IDENTITY_ID + `_${optionId}`,
]);
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
const expectedReceivedDiff = symmetricDifference(
expectedMenuItems,
MainContextMenuHandler["existingMenuItems"],
);
expect(expectedReceivedDiff.size).toEqual(0);
});
});
describe("removeBlockedUriMenuItems", () => {
it("removes menu items that require code injection", async () => {
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
autofillSettingsService.enableContextMenu$ = of(true);
stateService.getIsAuthenticated.mockResolvedValue(true);
const optionId = "1";
await sut.loadOptions("TEST_TITLE", optionId, createCipher());
await sut.removeBlockedUriMenuItems();
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledTimes(5);
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_ID);
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_IDENTITY_ID);
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(AUTOFILL_CARD_ID);
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(SEPARATOR_ID + 2);
expect(MainContextMenuHandler["remove"]).toHaveBeenCalledWith(COPY_IDENTIFIER_ID);
expect(MainContextMenuHandler["existingMenuItems"].size).toEqual(4);
const expectedMenuItems = new Set([
AUTOFILL_ID + `_${optionId}`,
COPY_PASSWORD_ID + `_${optionId}`,
COPY_USERNAME_ID + `_${optionId}`,
COPY_VERIFICATION_CODE_ID + `_${optionId}`,
]);
// @TODO Replace with `symmetricDifference` Set method once node 22.0.0 or higher is used
// const expectedReceivedDiff = expectedMenuItems.symmetricDifference(MainContextMenuHandler["existingMenuItems"])
const expectedReceivedDiff = symmetricDifference(
expectedMenuItems,
MainContextMenuHandler["existingMenuItems"],
);
expect(expectedReceivedDiff.size).toEqual(0);
});
});

View File

@@ -31,6 +31,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
export class MainContextMenuHandler {
static existingMenuItems: Set<string> = new Set();
initRunning = false;
private initContextMenuItems: InitContextMenuItems[] = [
{
@@ -41,6 +42,7 @@ export class MainContextMenuHandler {
id: AUTOFILL_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillLogin"),
requiresUnblockedUri: true,
},
{
id: COPY_USERNAME_ID,
@@ -56,7 +58,7 @@ export class MainContextMenuHandler {
id: COPY_VERIFICATION_CODE_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyVerificationCode"),
checkPremiumAccess: true,
requiresPremiumAccess: true,
},
{
id: SEPARATOR_ID + 1,
@@ -67,16 +69,19 @@ export class MainContextMenuHandler {
id: AUTOFILL_IDENTITY_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillIdentity"),
requiresUnblockedUri: true,
},
{
id: AUTOFILL_CARD_ID,
parentId: ROOT_ID,
title: this.i18nService.t("autoFillCard"),
requiresUnblockedUri: true,
},
{
id: SEPARATOR_ID + 2,
type: "separator",
parentId: ROOT_ID,
requiresUnblockedUri: true,
},
{
id: GENERATE_PASSWORD_ID,
@@ -87,6 +92,7 @@ export class MainContextMenuHandler {
id: COPY_IDENTIFIER_ID,
parentId: ROOT_ID,
title: this.i18nService.t("copyElementIdentifier"),
requiresUnblockedUri: true,
},
];
private noCardsContextMenuItems: chrome.contextMenus.CreateProperties[] = [
@@ -175,13 +181,19 @@ export class MainContextMenuHandler {
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
);
for (const options of this.initContextMenuItems) {
if (options.checkPremiumAccess && !hasPremium) {
for (const menuItem of this.initContextMenuItems) {
const {
requiresPremiumAccess,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requiresUnblockedUri, // destructuring this out of being passed to `create`
...otherOptions
} = menuItem;
if (requiresPremiumAccess && !hasPremium) {
continue;
}
delete options.checkPremiumAccess;
await MainContextMenuHandler.create({ ...options, contexts: ["all"] });
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
}
} catch (error) {
this.logService.warning(error.message);
@@ -202,12 +214,16 @@ export class MainContextMenuHandler {
}
return new Promise<void>((resolve, reject) => {
chrome.contextMenus.create(options, () => {
const itemId = chrome.contextMenus.create(options, () => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve();
});
this.existingMenuItems.add(`${itemId}`);
return itemId;
});
};
@@ -221,12 +237,16 @@ export class MainContextMenuHandler {
resolve();
});
this.existingMenuItems = new Set();
return;
});
}
static remove(menuItemId: string) {
return new Promise<void>((resolve, reject) => {
chrome.contextMenus.remove(menuItemId, () => {
const itemId = chrome.contextMenus.remove(menuItemId, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
@@ -234,6 +254,10 @@ export class MainContextMenuHandler {
resolve();
});
this.existingMenuItems.delete(`${itemId}`);
return;
});
}
@@ -244,6 +268,11 @@ export class MainContextMenuHandler {
const createChildItem = async (parentId: string) => {
const menuItemId = `${parentId}_${optionId}`;
const itemAlreadyExists = MainContextMenuHandler.existingMenuItems.has(menuItemId);
if (itemAlreadyExists) {
return;
}
return await MainContextMenuHandler.create({
type: "normal",
id: menuItemId,
@@ -255,10 +284,18 @@ export class MainContextMenuHandler {
if (
!cipher ||
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
(cipher.type === CipherType.Login &&
(!Utils.isNullOrEmpty(cipher.login?.username) ||
!Utils.isNullOrEmpty(cipher.login?.password) ||
!Utils.isNullOrEmpty(cipher.login?.totp)))
) {
await createChildItem(AUTOFILL_ID);
}
if (
!cipher ||
(cipher.type === CipherType.Login && !Utils.isNullOrEmpty(cipher.login?.password))
) {
if (cipher?.viewPassword ?? true) {
await createChildItem(COPY_PASSWORD_ID);
}
@@ -305,10 +342,22 @@ export class MainContextMenuHandler {
}
}
async removeBlockedUriMenuItems() {
try {
for (const menuItem of this.initContextMenuItems) {
if (menuItem.requiresUnblockedUri && menuItem.id) {
await MainContextMenuHandler.remove(menuItem.id);
}
}
} catch (error) {
this.logService.warning(error.message);
}
}
async noCards() {
try {
for (const option of this.noCardsContextMenuItems) {
await MainContextMenuHandler.create(option);
for (const menuItem of this.noCardsContextMenuItems) {
await MainContextMenuHandler.create(menuItem);
}
} catch (error) {
this.logService.warning(error.message);
@@ -317,8 +366,8 @@ export class MainContextMenuHandler {
async noIdentities() {
try {
for (const option of this.noIdentitiesContextMenuItems) {
await MainContextMenuHandler.create(option);
for (const menuItem of this.noIdentitiesContextMenuItems) {
await MainContextMenuHandler.create(menuItem);
}
} catch (error) {
this.logService.warning(error.message);
@@ -327,8 +376,8 @@ export class MainContextMenuHandler {
async noLogins() {
try {
for (const option of this.noLoginsContextMenuItems) {
await MainContextMenuHandler.create(option);
for (const menuItem of this.noLoginsContextMenuItems) {
await MainContextMenuHandler.create(menuItem);
}
await this.loadOptions(this.i18nService.t("addLoginMenu"), CREATE_LOGIN_ID);

View File

@@ -10,3 +10,4 @@ export { PartyHorn } from "./party-horn";
export { PencilSquare } from "./pencil-square";
export { Shield } from "./shield";
export { User } from "./user";
export { Warning } from "./warning";

View File

@@ -6,168 +6,262 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
export function PartyHorn({ theme }: { theme: Theme }) {
if (theme === ThemeTypes.Dark) {
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="50"
height="50"
viewBox="0 0 50 50"
fill="none"
>
<path
fill="#030C1B"
d="M33.044 37.587 5.716 48.972a2.275 2.275 0 0 1-2.975-2.975L14.127 18.67a4.31 4.31 0 0 1 5.217-2.47l.77.23a22.625 22.625 0 0 1 15.17 15.17l.23.77a4.31 4.31 0 0 1-2.47 5.217Z"
d="M32.6273 37.2714L3.88045 49.2492C2.98525 49.6222 1.95344 49.4181 1.26769 48.7323C0.581933 48.0466 0.377816 47.0148 0.750816 46.1196L12.7287 17.3728C13.622 15.2288 15.9911 14.1069 18.2158 14.7743L19.0257 15.0173C26.6887 17.3161 32.6839 23.3113 34.9828 30.9743L35.2257 31.7842C35.8931 34.0089 34.7712 36.3781 32.6273 37.2714Z"
fill="#FFBF00"
/>
<path
fill="#FFBF00"
fill-rule="evenodd"
d="M15.947 44.71a47.282 47.282 0 0 1-4.768-4.175 47.292 47.292 0 0 1-4.175-4.768l.48-1.154a45.963 45.963 0 0 0 4.457 5.16 45.967 45.967 0 0 0 5.16 4.456l-1.154.481ZM11.4 46.604a56.295 56.295 0 0 1-6.292-6.291l-.463 1.112a57.493 57.493 0 0 0 5.642 5.643l1.113-.464Zm10.65-4.437c-2.38-1.42-4.765-3.271-7-5.505-2.233-2.234-4.084-4.62-5.504-6.999l.52-1.25c1.414 2.52 3.347 5.087 5.747 7.487 2.4 2.4 4.966 4.332 7.486 5.746l-1.25.52Zm-9.634-19.393c.894 3.259 3.09 6.93 6.342 10.181 3.251 3.252 6.922 5.448 10.18 6.341l1.747-.727a12.588 12.588 0 0 1-1.756-.396c-2.975-.885-6.364-2.934-9.41-5.98-3.045-3.045-5.094-6.434-5.98-9.41a12.584 12.584 0 0 1-.395-1.755l-.728 1.746Z"
clip-rule="evenodd"
d="M14.6426 44.7649C12.9332 43.4627 11.2495 41.9951 9.62721 40.3728C8.00492 38.7505 6.53732 37.0668 5.23507 35.3574L5.74088 34.1434C7.10571 35.9897 8.67231 37.8151 10.4286 39.5714C12.1848 41.3277 14.0103 42.8943 15.8566 44.2591L14.6426 44.7649ZM9.86079 46.7574C8.69632 45.7641 7.54698 44.7037 6.42165 43.5783C5.29632 42.453 4.23589 41.3037 3.24264 40.1392L2.755 41.3095C3.65901 42.3487 4.6147 43.3741 5.62028 44.3797C6.62586 45.3853 7.65127 46.341 8.69048 47.245L9.86079 46.7574ZM21.0629 42.0898C18.5607 40.5957 16.0508 38.6488 13.701 36.299C11.3512 33.9492 9.40432 31.4393 7.91017 28.9371L8.45815 27.622C9.94515 30.2728 11.9779 32.973 14.5024 35.4976C17.027 38.0221 19.7272 40.0548 22.378 41.5418L21.0629 42.0898ZM10.9297 21.6902C11.8698 25.118 14.18 28.9793 17.6004 32.3996C21.0207 35.82 24.882 38.1302 28.3098 39.0703L30.1472 38.3047C29.5643 38.2191 28.9477 38.0815 28.3004 37.8889C25.1702 36.9575 21.6052 34.8017 18.4018 31.5982C15.1983 28.3948 13.0425 24.8297 12.1111 21.6996C11.9185 21.0523 11.7809 20.4357 11.6953 19.8528L10.9297 21.6902Z"
fill="#F3F6F9"
/>
<path
fill="#0E3377"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
d="M27.985 23.73c2.174 2.174 3.79 4.462 4.653 6.387.432.965.663 1.811.699 2.491.035.68-.125 1.13-.4 1.406-.276.275-.726.436-1.406.4-.68-.035-1.526-.267-2.49-.699-1.926-.863-4.214-2.478-6.389-4.653-2.175-2.175-3.79-4.463-4.653-6.388-.432-.965-.664-1.811-.7-2.49-.035-.68.126-1.131.401-1.407.275-.275.726-.436 1.406-.4.68.036 1.526.267 2.49.7 1.926.862 4.214 2.478 6.389 4.652Z"
d="M27.706 22.294C32.3531 26.9411 34.6852 32.1435 32.9149 33.9138C31.1445 35.6842 25.9421 33.3521 21.295 28.7049C16.6479 24.0578 14.3158 18.8554 16.0861 17.085C17.8564 15.3147 23.0588 17.6468 27.706 22.294Z"
fill="#79A1E9"
/>
<path
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
d="M33.044 37.587 5.716 48.972a2.275 2.275 0 0 1-2.975-2.975L14.127 18.67a4.31 4.31 0 0 1 5.217-2.47l.77.23a22.625 22.625 0 0 1 15.17 15.17l.23.77a4.31 4.31 0 0 1-2.47 5.217Z"
fill-rule="evenodd"
clip-rule="evenodd"
d="M31.6828 29.6463C30.8097 27.6986 29.1563 25.347 26.9046 23.0953C24.6529 20.8436 22.3013 19.1902 20.3536 18.3171C19.376 17.8788 18.5567 17.6628 17.9359 17.6303C17.3148 17.5979 17.0236 17.7503 16.8875 17.8864C16.7514 18.0225 16.5989 18.3138 16.6314 18.9349C16.6638 19.5556 16.8799 20.3749 17.3182 21.3526C18.1912 23.3003 19.8447 25.6519 22.0964 27.9035C24.3481 30.1552 26.6996 31.8087 28.6473 32.6818C29.625 33.12 30.4443 33.3361 31.0651 33.3685C31.6862 33.401 31.9774 33.2486 32.1135 33.1125C32.2496 32.9763 32.402 32.6851 32.3696 32.064C32.3371 31.4433 32.1211 30.624 31.6828 29.6463ZM32.9149 33.9138C34.6852 32.1435 32.3531 26.9411 27.706 22.294C23.0588 17.6468 17.8564 15.3147 16.0861 17.085C14.3158 18.8554 16.6479 24.0578 21.295 28.7049C25.9421 33.3521 31.1445 35.6842 32.9149 33.9138Z"
fill="#175DDC"
/>
<path
fill="#91B4F2"
d="M25.46 2.47a.407.407 0 0 1 .793 0l.02.086a3.232 3.232 0 0 0 2.415 2.414l.086.02a.407.407 0 0 1 0 .793l-.086.02a3.232 3.232 0 0 0-2.414 2.415l-.02.086a.407.407 0 0 1-.794 0l-.02-.086a3.232 3.232 0 0 0-2.414-2.414l-.087-.02a.407.407 0 0 1 0-.794l.087-.02a3.232 3.232 0 0 0 2.414-2.414l.02-.086ZM45.93 10.55a.407.407 0 0 1 .794 0l.02.086a3.232 3.232 0 0 0 2.414 2.415l.086.02a.407.407 0 0 1 0 .793l-.086.02a3.232 3.232 0 0 0-2.414 2.414l-.02.087a.407.407 0 0 1-.794 0l-.02-.087a3.232 3.232 0 0 0-2.414-2.414l-.086-.02a.407.407 0 0 1 0-.793l.086-.02a3.232 3.232 0 0 0 2.414-2.415l.02-.086ZM38.928 43.41a.407.407 0 0 1 .793 0l.02.086a3.232 3.232 0 0 0 2.414 2.414l.086.02a.407.407 0 0 1 0 .794l-.086.02a3.232 3.232 0 0 0-2.414 2.414l-.02.086a.407.407 0 0 1-.793 0l-.02-.086a3.232 3.232 0 0 0-2.415-2.414l-.086-.02a.407.407 0 0 1 0-.793l.086-.02a3.232 3.232 0 0 0 2.414-2.415l.02-.086Z"
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.053 15.3169C16.1064 14.7329 14.0334 15.7146 13.2517 17.5906L1.2739 46.3374C0.989218 47.0206 1.14501 47.8081 1.66839 48.3315C2.19178 48.8549 2.97928 49.0107 3.66253 48.726L32.4093 36.7482C34.2853 35.9665 35.267 33.8935 34.683 31.9469L34.44 31.137C32.1959 23.6565 26.3434 17.804 18.8629 15.5599L18.053 15.3169ZM12.2056 17.1547C13.2106 14.7428 15.8759 13.4806 18.3786 14.2314L19.1886 14.4744C27.034 16.828 33.1719 22.9659 35.5256 30.8113L35.7685 31.6213C36.5193 34.124 35.2571 36.7893 32.8452 37.7943L4.09841 49.7721C2.99125 50.2335 1.71514 49.981 0.867022 49.1329C0.0189018 48.2848 -0.233545 47.0087 0.227771 45.9015L12.2056 17.1547Z"
fill="#175DDC"
/>
<path
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
d="M37.708 27.827a4.31 4.31 0 0 1 6.095 0M36.63 24.873a6.95 6.95 0 0 1 9.495-2.544M17.238 13.467a4.31 4.31 0 0 0-4.31-4.31M18.583 10.392a4.31 4.31 0 0 0-2.533-5.544"
d="M24.65 0.331121C24.6952 0.137206 24.8681 0 25.0672 0C25.2663 0 25.4391 0.137206 25.4843 0.331121L25.5055 0.421998C25.7994 1.68306 26.784 2.66773 28.0451 2.9616L28.136 2.98277C28.3299 3.02796 28.4671 3.20082 28.4671 3.39993C28.4671 3.59904 28.3299 3.77189 28.136 3.81708L28.0451 3.83826C26.784 4.13212 25.7994 5.11679 25.5055 6.37786L25.4843 6.46873C25.4391 6.66265 25.2663 6.79985 25.0672 6.79985C24.8681 6.79985 24.6952 6.66265 24.65 6.46873L24.6288 6.37786C24.335 5.11679 23.3503 4.13212 22.0892 3.83826L21.9984 3.81708C21.8044 3.77189 21.6672 3.59904 21.6672 3.39993C21.6672 3.20082 21.8044 3.02796 21.9984 2.98277L22.0892 2.9616C23.3503 2.66773 24.335 1.68306 24.6288 0.421997L24.65 0.331121Z"
fill="#175DDC"
/>
<circle
cx="32.86"
cy="11.313"
r="1.616"
fill="#0E3377"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
<path
d="M46.183 8.83088C46.2282 8.63696 46.401 8.49976 46.6001 8.49976C46.7992 8.49976 46.9721 8.63696 47.0173 8.83088L47.0385 8.92175C47.3323 10.1828 48.317 11.1675 49.5781 11.4614L49.6689 11.4825C49.8628 11.5277 50 11.7006 50 11.8997C50 12.0988 49.8628 12.2716 49.6689 12.3168L49.5781 12.338C48.317 12.6319 47.3323 13.6165 47.0385 14.8776L47.0173 14.9685C46.9721 15.1624 46.7992 15.2996 46.6001 15.2996C46.401 15.2996 46.2282 15.1624 46.183 14.9685L46.1618 14.8776C45.8679 13.6165 44.8833 12.6319 43.6222 12.338L43.5313 12.3168C43.3374 12.2716 43.2002 12.0988 43.2002 11.8997C43.2002 11.7006 43.3374 11.5277 43.5313 11.4825L43.6222 11.4614C44.8833 11.1675 45.8679 10.1828 46.1618 8.92175L46.183 8.83088Z"
fill="#175DDC"
/>
<circle
cx="36.631"
cy="17.777"
r="1.077"
fill="#030C1B"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
<path
d="M38.8164 43.3968C38.8616 43.2029 39.0344 43.0657 39.2335 43.0657C39.4327 43.0657 39.6055 43.2029 39.6507 43.3968L39.6719 43.4877C39.9657 44.7487 40.9504 45.7334 42.2115 46.0273L42.3024 46.0484C42.4963 46.0936 42.6335 46.2665 42.6335 46.4656C42.6335 46.6647 42.4963 46.8376 42.3024 46.8828L42.2115 46.9039C40.9504 47.1978 39.9657 48.1825 39.6719 49.4435L39.6507 49.5344C39.6055 49.7283 39.4327 49.8655 39.2335 49.8655C39.0344 49.8655 38.8616 49.7283 38.8164 49.5344L38.7952 49.4435C38.5013 48.1825 37.5167 47.1978 36.2556 46.9039L36.1647 46.8828C35.9708 46.8376 35.8336 46.6647 35.8336 46.4656C35.8336 46.2665 35.9708 46.0936 36.1647 46.0484L36.2556 46.0273C37.5167 45.7334 38.5013 44.7487 38.7952 43.4877L38.8164 43.3968Z"
fill="#175DDC"
/>
<circle
cx="30.705"
cy="44.172"
r="1.077"
fill="#030C1B"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M43.5439 27.4056C41.9949 25.8565 39.4834 25.8565 37.9343 27.4056C37.713 27.6269 37.3543 27.6269 37.133 27.4056C36.9117 27.1843 36.9117 26.8255 37.133 26.6042C39.1246 24.6126 42.3537 24.6126 44.3453 26.6042C44.5666 26.8255 44.5666 27.1843 44.3453 27.4056C44.124 27.6269 43.7652 27.6269 43.5439 27.4056Z"
fill="#175DDC"
/>
<circle
cx="44.711"
cy="34.476"
r="2.155"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M46.1043 21.7127C42.8784 19.8502 38.7535 20.9555 36.891 24.1814C36.7346 24.4524 36.388 24.5452 36.117 24.3888C35.8459 24.2323 35.7531 23.8857 35.9096 23.6147C38.085 19.8468 42.903 18.5558 46.671 20.7312C46.942 20.8877 47.0349 21.2342 46.8784 21.5053C46.7219 21.7763 46.3753 21.8691 46.1043 21.7127Z"
fill="#175DDC"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4675 7.93311C13.6582 7.93311 15.4341 9.70901 15.4341 11.8997C15.4341 12.2127 15.6878 12.4664 16.0007 12.4664C16.3137 12.4664 16.5674 12.2127 16.5674 11.8997C16.5674 9.0831 14.2841 6.7998 11.4675 6.7998C11.1545 6.7998 10.9008 7.0535 10.9008 7.36646C10.9008 7.67941 11.1545 7.93311 11.4675 7.93311Z"
fill="#175DDC"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.553 3.36429C16.6057 4.12949 17.6494 6.41384 16.8842 8.46654C16.7749 8.75978 16.924 9.08612 17.2173 9.19543C17.5105 9.30475 17.8368 9.15564 17.9462 8.8624C18.93 6.22322 17.5881 3.28619 14.9489 2.30236C14.6556 2.19305 14.3293 2.34215 14.22 2.63539C14.1107 2.92864 14.2598 3.25497 14.553 3.36429Z"
fill="#175DDC"
/>
<path
d="M34.7004 9.63307C34.7004 10.8849 33.6856 11.8997 32.4337 11.8997C31.1819 11.8997 30.1671 10.8849 30.1671 9.63307C30.1671 8.38125 31.1819 7.36646 32.4337 7.36646C33.6856 7.36646 34.7004 8.38125 34.7004 9.63307Z"
fill="#79A1E9"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M32.4337 10.7664C33.0596 10.7664 33.567 10.259 33.567 9.63307C33.567 9.00716 33.0596 8.49976 32.4337 8.49976C31.8078 8.49976 31.3004 9.00716 31.3004 9.63307C31.3004 10.259 31.8078 10.7664 32.4337 10.7664ZM32.4337 11.8997C33.6856 11.8997 34.7004 10.8849 34.7004 9.63307C34.7004 8.38125 33.6856 7.36646 32.4337 7.36646C31.1819 7.36646 30.1671 8.38125 30.1671 9.63307C30.1671 10.8849 31.1819 11.8997 32.4337 11.8997Z"
fill="#175DDC"
/>
<path
d="M38.1002 16.4329C38.1002 17.3717 37.3391 18.1328 36.4003 18.1328C35.4614 18.1328 34.7003 17.3717 34.7003 16.4329C34.7003 15.494 35.4614 14.7329 36.4003 14.7329C37.3391 14.7329 38.1002 15.494 38.1002 16.4329Z"
fill="#AAC3EF"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M36.4003 16.9995C36.7132 16.9995 36.9669 16.7458 36.9669 16.4329C36.9669 16.1199 36.7132 15.8662 36.4003 15.8662C36.0873 15.8662 35.8336 16.1199 35.8336 16.4329C35.8336 16.7458 36.0873 16.9995 36.4003 16.9995ZM36.4003 18.1328C37.3391 18.1328 38.1002 17.3717 38.1002 16.4329C38.1002 15.494 37.3391 14.7329 36.4003 14.7329C35.4614 14.7329 34.7003 15.494 34.7003 16.4329C34.7003 17.3717 35.4614 18.1328 36.4003 18.1328Z"
fill="#175DDC"
/>
<path
d="M31.8671 44.199C31.8671 45.1379 31.106 45.899 30.1671 45.899C29.2283 45.899 28.4672 45.1379 28.4672 44.199C28.4672 43.2601 29.2283 42.499 30.1671 42.499C31.106 42.499 31.8671 43.2601 31.8671 44.199Z"
fill="#AAC3EF"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M30.1671 44.7656C30.4801 44.7656 30.7338 44.5119 30.7338 44.199C30.7338 43.886 30.4801 43.6323 30.1671 43.6323C29.8542 43.6323 29.6005 43.886 29.6005 44.199C29.6005 44.5119 29.8542 44.7656 30.1671 44.7656ZM30.1671 45.899C31.106 45.899 31.8671 45.1379 31.8671 44.199C31.8671 43.2601 31.106 42.499 30.1671 42.499C29.2283 42.499 28.4672 43.2601 28.4672 44.199C28.4672 45.1379 29.2283 45.899 30.1671 45.899Z"
fill="#175DDC"
/>
<path
d="M47.7334 33.9993C47.7334 35.5641 46.4649 36.8326 44.9002 36.8326C43.3354 36.8326 42.0669 35.5641 42.0669 33.9993C42.0669 32.4345 43.3354 31.166 44.9002 31.166C46.4649 31.166 47.7334 32.4345 47.7334 33.9993Z"
fill="#FFBF00"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
/>
<circle
cx="41.479"
cy="5.926"
r="3.232"
fill="#202733"
stroke="#91B4F2"
stroke-linecap="round"
stroke-width="1.077"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M44.9002 35.6993C45.839 35.6993 46.6001 34.9382 46.6001 33.9993C46.6001 33.0604 45.839 32.2993 44.9002 32.2993C43.9613 32.2993 43.2002 33.0604 43.2002 33.9993C43.2002 34.9382 43.9613 35.6993 44.9002 35.6993ZM44.9002 36.8326C46.4649 36.8326 47.7334 35.5641 47.7334 33.9993C47.7334 32.4345 46.4649 31.166 44.9002 31.166C43.3354 31.166 42.0669 32.4345 42.0669 33.9993C42.0669 35.5641 43.3354 36.8326 44.9002 36.8326Z"
fill="#175DDC"
/>
<path
d="M29.0337 13.3163C29.0337 14.4116 28.1458 15.2996 27.0504 15.2996C25.9551 15.2996 25.0671 14.4116 25.0671 13.3163C25.0671 12.221 25.9551 11.333 27.0504 11.333C28.1458 11.333 29.0337 12.221 29.0337 13.3163Z"
fill="#FFBF00"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.0504 14.1663C27.5199 14.1663 27.9004 13.7857 27.9004 13.3163C27.9004 12.8469 27.5199 12.4663 27.0504 12.4663C26.581 12.4663 26.2004 12.8469 26.2004 13.3163C26.2004 13.7857 26.581 14.1663 27.0504 14.1663ZM27.0504 15.2996C28.1458 15.2996 29.0337 14.4116 29.0337 13.3163C29.0337 12.221 28.1458 11.333 27.0504 11.333C25.9551 11.333 25.0671 12.221 25.0671 13.3163C25.0671 14.4116 25.9551 15.2996 27.0504 15.2996Z"
fill="#175DDC"
/>
<path
d="M45.4667 3.96658C45.4667 6.15726 43.6908 7.93316 41.5002 7.93316C39.3095 7.93316 37.5336 6.15726 37.5336 3.96658C37.5336 1.7759 39.3095 0 41.5002 0C43.6908 0 45.4667 1.7759 45.4667 3.96658Z"
fill="#F3F6F9"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.5002 6.79985C43.0649 6.79985 44.3334 5.53136 44.3334 3.96658C44.3334 2.40181 43.0649 1.13331 41.5002 1.13331C39.9354 1.13331 38.6669 2.40181 38.6669 3.96658C38.6669 5.53136 39.9354 6.79985 41.5002 6.79985ZM41.5002 7.93316C43.6908 7.93316 45.4667 6.15726 45.4667 3.96658C45.4667 1.7759 43.6908 0 41.5002 0C39.3095 0 37.5336 1.7759 37.5336 3.96658C37.5336 6.15726 39.3095 7.93316 41.5002 7.93316Z"
fill="#175DDC"
/>
</svg>
`;
}
return html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" fill="none">
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
<path
fill="#DFE9FB"
d="M33.044 37.587 5.716 48.972a2.275 2.275 0 0 1-2.975-2.975L14.127 18.67a4.31 4.31 0 0 1 5.217-2.47l.77.23a22.625 22.625 0 0 1 15.17 15.17l.23.77a4.31 4.31 0 0 1-2.47 5.217Z"
d="M32.6275 37.2714L3.88069 49.2492C2.98549 49.6222 1.95368 49.4181 1.26793 48.7323C0.582178 48.0466 0.37806 47.0148 0.751061 46.1196L12.7289 17.3728C13.6222 15.2288 15.9914 14.1069 18.216 14.7743L19.026 15.0173C26.6889 17.3161 32.6841 23.3113 34.983 30.9743L35.226 31.7842C35.8934 34.0089 34.7714 36.3781 32.6275 37.2714Z"
fill="#FFBF00"
/>
<path
fill="#FFBF00"
fill-rule="evenodd"
d="M15.947 44.71a47.27 47.27 0 0 1-4.768-4.175 47.284 47.284 0 0 1-4.175-4.768l.48-1.154a45.953 45.953 0 0 0 4.457 5.16 45.958 45.958 0 0 0 5.16 4.456l-1.154.481ZM11.4 46.604a56.295 56.295 0 0 1-6.292-6.291l-.463 1.112a57.493 57.493 0 0 0 5.642 5.643l1.113-.464Zm10.65-4.437c-2.38-1.42-4.765-3.271-7-5.505-2.233-2.234-4.084-4.62-5.504-6.999l.52-1.25c1.414 2.52 3.347 5.087 5.747 7.487 2.4 2.4 4.966 4.332 7.486 5.746l-1.25.52Zm-9.634-19.393c.894 3.259 3.09 6.93 6.342 10.181 3.251 3.252 6.922 5.448 10.18 6.341l1.747-.727a12.586 12.586 0 0 1-1.756-.396c-2.975-.885-6.364-2.934-9.41-5.98-3.045-3.045-5.094-6.434-5.98-9.41a12.585 12.585 0 0 1-.395-1.755l-.728 1.746Z"
clip-rule="evenodd"
d="M14.6425 44.7649C12.9331 43.4627 11.2494 41.9951 9.62709 40.3728C8.00479 38.7505 6.53719 37.0668 5.23494 35.3574L5.74076 34.1434C7.10558 35.9897 8.67219 37.8151 10.4285 39.5714C12.1847 41.3277 14.0102 42.8943 15.8565 44.2591L14.6425 44.7649ZM9.86067 46.7574C8.6962 45.7641 7.54686 44.7037 6.42153 43.5783C5.2962 42.453 4.23577 41.3037 3.24251 40.1392L2.75488 41.3095C3.65889 42.3487 4.61458 43.3741 5.62016 44.3797C6.62574 45.3853 7.65114 46.341 8.69036 47.245L9.86067 46.7574ZM21.0628 42.0898C18.5605 40.5957 16.0507 38.6488 13.7009 36.299C11.3511 33.9492 9.40419 31.4393 7.91005 28.9371L8.45802 27.622C9.94503 30.2728 11.9777 32.973 14.5023 35.4976C17.0268 38.0221 19.7271 40.0548 22.3779 41.5418L21.0628 42.0898ZM10.9296 21.6902C11.8696 25.118 14.1799 28.9793 17.6003 32.3996C21.0206 35.82 24.8819 38.1302 28.3097 39.0703L30.1471 38.3047C29.5641 38.2191 28.9476 38.0815 28.3003 37.8889C25.1701 36.9575 21.6051 34.8017 18.4016 31.5982C15.1982 28.3948 13.0424 24.8297 12.111 21.6996C11.9184 21.0523 11.7808 20.4357 11.6952 19.8528L10.9296 21.6902Z"
fill="white"
/>
<path
d="M27.7062 22.294C32.3534 26.9411 34.6855 32.1435 32.9151 33.9138C31.1448 35.6842 25.9424 33.3521 21.2952 28.7049C16.6481 24.0578 14.316 18.8554 16.0863 17.085C17.8567 15.3147 23.0591 17.6468 27.7062 22.294Z"
fill="#99BAF4"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
d="M27.985 23.73c2.175 2.174 3.79 4.462 4.653 6.387.432.965.663 1.811.699 2.491.036.68-.125 1.13-.4 1.406-.276.275-.726.436-1.406.4-.68-.035-1.526-.267-2.49-.699-1.926-.863-4.214-2.478-6.389-4.653-2.175-2.175-3.79-4.463-4.653-6.388-.432-.965-.664-1.811-.7-2.49-.035-.68.126-1.131.401-1.407.275-.275.726-.436 1.406-.4.68.036 1.526.267 2.49.7 1.926.862 4.214 2.478 6.389 4.652Z"
/>
<path
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
d="M33.044 37.587 5.716 48.972a2.275 2.275 0 0 1-2.975-2.975L14.127 18.67a4.31 4.31 0 0 1 5.217-2.47l.77.23a22.625 22.625 0 0 1 15.17 15.17l.23.77a4.31 4.31 0 0 1-2.47 5.217Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M31.6831 29.6463C30.81 27.6986 29.1565 25.347 26.9048 23.0953C24.6532 20.8436 22.3016 19.1902 20.3539 18.3171C19.3762 17.8788 18.5569 17.6628 17.9362 17.6303C17.3151 17.5979 17.0238 17.7503 16.8877 17.8864C16.7516 18.0225 16.5992 18.3138 16.6316 18.9349C16.6641 19.5556 16.8801 20.3749 17.3184 21.3526C18.1915 23.3003 19.8449 25.6519 22.0966 27.9035C24.3483 30.1552 26.6999 31.8087 28.6476 32.6818C29.6253 33.12 30.4446 33.3361 31.0653 33.3685C31.6864 33.401 31.9776 33.2486 32.1138 33.1125C32.2499 32.9763 32.4023 32.6851 32.3698 32.064C32.3374 31.4433 32.1213 30.624 31.6831 29.6463ZM32.9151 33.9138C34.6855 32.1435 32.3534 26.9411 27.7062 22.294C23.0591 17.6468 17.8567 15.3147 16.0863 17.085C14.316 18.8554 16.6481 24.0578 21.2952 28.7049C25.9424 33.3521 31.1448 35.6842 32.9151 33.9138Z"
fill="#0E3781"
d="M25.46 2.47a.407.407 0 0 1 .793 0l.02.086a3.232 3.232 0 0 0 2.415 2.414l.086.02a.407.407 0 0 1 0 .793l-.086.02a3.232 3.232 0 0 0-2.414 2.415l-.02.086a.407.407 0 0 1-.794 0l-.02-.086a3.232 3.232 0 0 0-2.414-2.414l-.086-.02a.407.407 0 0 1 0-.794l.086-.02a3.232 3.232 0 0 0 2.414-2.414l.02-.086ZM45.93 10.55a.407.407 0 0 1 .794 0l.02.086a3.232 3.232 0 0 0 2.414 2.415l.086.02a.407.407 0 0 1 0 .793l-.086.02a3.232 3.232 0 0 0-2.414 2.414l-.02.087a.407.407 0 0 1-.794 0l-.02-.087a3.232 3.232 0 0 0-2.414-2.414l-.086-.02a.407.407 0 0 1 0-.793l.086-.02a3.232 3.232 0 0 0 2.414-2.415l.02-.086ZM38.928 43.41a.407.407 0 0 1 .793 0l.02.086a3.232 3.232 0 0 0 2.414 2.414l.086.02a.407.407 0 0 1 0 .794l-.086.02a3.232 3.232 0 0 0-2.414 2.414l-.02.086a.407.407 0 0 1-.793 0l-.02-.086a3.232 3.232 0 0 0-2.415-2.414l-.086-.02a.407.407 0 0 1 0-.793l.086-.02a3.232 3.232 0 0 0 2.414-2.415l.02-.086Z"
/>
<path
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
d="M37.708 27.827a4.31 4.31 0 0 1 6.095 0M36.63 24.873a6.95 6.95 0 0 1 9.495-2.544M17.238 13.467a4.31 4.31 0 0 0-4.31-4.31M18.583 10.392a4.31 4.31 0 0 0-2.533-5.544"
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.053 15.317C16.1064 14.733 14.0334 15.7148 13.2517 17.5907L1.2739 46.3375C0.989218 47.0207 1.14501 47.8083 1.66839 48.3316C2.19178 48.855 2.97928 49.0108 3.66253 48.7261L32.4093 36.7483C34.2853 35.9666 35.267 33.8936 34.683 31.947L34.44 31.1371C32.1959 23.6566 26.3434 17.8041 18.8629 15.56L18.053 15.317ZM12.2056 17.1548C13.2106 14.7429 15.8759 13.4807 18.3786 14.2315L19.1886 14.4745C27.034 16.8281 33.1719 22.966 35.5256 30.8115L35.7685 31.6214C36.5193 34.1241 35.2571 36.7895 32.8452 37.7944L4.09841 49.7723C2.99125 50.2336 1.71514 49.9811 0.867022 49.133C0.0189018 48.2849 -0.233545 47.0088 0.227771 45.9016L12.2056 17.1548Z"
fill="#0E3781"
/>
<circle
cx="32.86"
cy="11.313"
r="1.616"
<path
d="M24.6503 0.331121C24.6954 0.137206 24.8683 0 25.0674 0C25.2665 0 25.4394 0.137206 25.4846 0.331121L25.5057 0.421998C25.7996 1.68306 26.7843 2.66773 28.0453 2.9616L28.1362 2.98277C28.3301 3.02796 28.4673 3.20082 28.4673 3.39993C28.4673 3.59904 28.3301 3.77189 28.1362 3.81708L28.0453 3.83826C26.7843 4.13212 25.7996 5.11679 25.5057 6.37786L25.4846 6.46873C25.4394 6.66265 25.2665 6.79985 25.0674 6.79985C24.8683 6.79985 24.6954 6.66265 24.6503 6.46873L24.6291 6.37786C24.3352 5.11679 23.3505 4.13212 22.0895 3.83826L21.9986 3.81708C21.8047 3.77189 21.6675 3.59904 21.6675 3.39993C21.6675 3.20082 21.8047 3.02796 21.9986 2.98277L22.0895 2.9616C23.3505 2.66773 24.3352 1.68306 24.6291 0.421997L24.6503 0.331121Z"
fill="#0E3781"
/>
<path
d="M46.183 8.831C46.2282 8.63708 46.401 8.49988 46.6001 8.49988C46.7992 8.49988 46.9721 8.63708 47.0173 8.831L47.0385 8.92188C47.3323 10.1829 48.317 11.1676 49.5781 11.4615L49.6689 11.4827C49.8628 11.5278 50 11.7007 50 11.8998C50 12.0989 49.8628 12.2718 49.6689 12.317L49.5781 12.3381C48.317 12.632 47.3323 13.6167 47.0385 14.8777L47.0173 14.9686C46.9721 15.1625 46.7992 15.2997 46.6001 15.2997C46.401 15.2997 46.2282 15.1625 46.183 14.9686L46.1618 14.8777C45.8679 13.6167 44.8833 12.632 43.6222 12.3381L43.5313 12.317C43.3374 12.2718 43.2002 12.0989 43.2002 11.8998C43.2002 11.7007 43.3374 11.5278 43.5313 11.4827L43.6222 11.4615C44.8833 11.1676 45.8679 10.1829 46.1618 8.92188L46.183 8.831Z"
fill="#0E3781"
/>
<path
d="M38.8163 43.3969C38.8615 43.203 39.0343 43.0658 39.2334 43.0658C39.4325 43.0658 39.6054 43.203 39.6506 43.3969L39.6718 43.4878C39.9656 44.7489 40.9503 45.7335 42.2114 46.0274L42.3022 46.0486C42.4961 46.0938 42.6334 46.2666 42.6334 46.4657C42.6334 46.6648 42.4961 46.8377 42.3022 46.8829L42.2114 46.9041C40.9503 47.1979 39.9656 48.1826 39.6718 49.4437L39.6506 49.5345C39.6054 49.7284 39.4325 49.8657 39.2334 49.8657C39.0343 49.8657 38.8615 49.7284 38.8163 49.5345L38.7951 49.4437C38.5012 48.1826 37.5166 47.1979 36.2555 46.9041L36.1646 46.8829C35.9707 46.8377 35.8335 46.6648 35.8335 46.4657C35.8335 46.2666 35.9707 46.0938 36.1646 46.0486L36.2555 46.0274C37.5166 45.7335 38.5012 44.7489 38.7951 43.4878L38.8163 43.3969Z"
fill="#0E3781"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M43.5441 27.4057C41.995 25.8567 39.4835 25.8567 37.9345 27.4057C37.7132 27.627 37.3544 27.627 37.1331 27.4057C36.9118 27.1844 36.9118 26.8256 37.1331 26.6043C39.1247 24.6127 42.3538 24.6127 44.3454 26.6043C44.5667 26.8256 44.5667 27.1844 44.3454 27.4057C44.1241 27.627 43.7653 27.627 43.5441 27.4057Z"
fill="#0E3781"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M46.1044 21.7127C42.8786 19.8502 38.7536 20.9555 36.8912 24.1814C36.7347 24.4524 36.3881 24.5452 36.1171 24.3888C35.8461 24.2323 35.7532 23.8857 35.9097 23.6147C38.0851 19.8468 42.9032 18.5558 46.6711 20.7312C46.9421 20.8877 47.035 21.2342 46.8785 21.5053C46.722 21.7763 46.3755 21.8691 46.1044 21.7127Z"
fill="#0E3781"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4675 7.93311C13.6582 7.93311 15.4341 9.70901 15.4341 11.8997C15.4341 12.2127 15.6878 12.4664 16.0007 12.4664C16.3137 12.4664 16.5674 12.2127 16.5674 11.8997C16.5674 9.0831 14.2841 6.7998 11.4675 6.7998C11.1545 6.7998 10.9008 7.0535 10.9008 7.36646C10.9008 7.67941 11.1545 7.93311 11.4675 7.93311Z"
fill="#0E3781"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.553 3.36429C16.6057 4.12949 17.6494 6.41384 16.8842 8.46654C16.7749 8.75978 16.924 9.08612 17.2173 9.19543C17.5105 9.30475 17.8368 9.15564 17.9462 8.8624C18.93 6.22322 17.5881 3.28619 14.9489 2.30236C14.6556 2.19305 14.3293 2.34215 14.22 2.63539C14.1107 2.92864 14.2598 3.25497 14.553 3.36429Z"
fill="#0E3781"
/>
<path
d="M34.7002 9.63307C34.7002 10.8849 33.6854 11.8997 32.4336 11.8997C31.1818 11.8997 30.167 10.8849 30.167 9.63307C30.167 8.38125 31.1818 7.36646 32.4336 7.36646C33.6854 7.36646 34.7002 8.38125 34.7002 9.63307Z"
fill="#99BAF4"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
/>
<circle
cx="36.631"
cy="17.777"
r="1.077"
fill="#DFE9FB"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M32.4336 10.7664C33.0595 10.7664 33.5669 10.259 33.5669 9.63307C33.5669 9.00716 33.0595 8.49976 32.4336 8.49976C31.8077 8.49976 31.3003 9.00716 31.3003 9.63307C31.3003 10.259 31.8077 10.7664 32.4336 10.7664ZM32.4336 11.8997C33.6854 11.8997 34.7002 10.8849 34.7002 9.63307C34.7002 8.38125 33.6854 7.36646 32.4336 7.36646C31.1818 7.36646 30.167 8.38125 30.167 9.63307C30.167 10.8849 31.1818 11.8997 32.4336 11.8997Z"
fill="#0E3781"
/>
<circle
cx="30.705"
cy="44.172"
r="1.077"
fill="#DFE9FB"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
<path
d="M38.1001 16.433C38.1001 17.3719 37.339 18.133 36.4002 18.133C35.4613 18.133 34.7002 17.3719 34.7002 16.433C34.7002 15.4941 35.4613 14.733 36.4002 14.733C37.339 14.733 38.1001 15.4941 38.1001 16.433Z"
fill="#DBE5F6"
/>
<circle
cx="44.711"
cy="34.476"
r="2.155"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M36.4002 16.9997C36.7131 16.9997 36.9668 16.746 36.9668 16.433C36.9668 16.12 36.7131 15.8663 36.4002 15.8663C36.0872 15.8663 35.8335 16.12 35.8335 16.433C35.8335 16.746 36.0872 16.9997 36.4002 16.9997ZM36.4002 18.133C37.339 18.133 38.1001 17.3719 38.1001 16.433C38.1001 15.4941 37.339 14.733 36.4002 14.733C35.4613 14.733 34.7002 15.4941 34.7002 16.433C34.7002 17.3719 35.4613 18.133 36.4002 18.133Z"
fill="#0E3781"
/>
<path
d="M31.8672 44.1991C31.8672 45.138 31.1061 45.8991 30.1672 45.8991C29.2284 45.8991 28.4673 45.138 28.4673 44.1991C28.4673 43.2602 29.2284 42.4991 30.1672 42.4991C31.1061 42.4991 31.8672 43.2602 31.8672 44.1991Z"
fill="#DBE5F6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M30.1672 44.7658C30.4802 44.7658 30.7339 44.5121 30.7339 44.1991C30.7339 43.8862 30.4802 43.6325 30.1672 43.6325C29.8543 43.6325 29.6006 43.8862 29.6006 44.1991C29.6006 44.5121 29.8543 44.7658 30.1672 44.7658ZM30.1672 45.8991C31.1061 45.8991 31.8672 45.138 31.8672 44.1991C31.8672 43.2602 31.1061 42.4991 30.1672 42.4991C29.2284 42.4991 28.4673 43.2602 28.4673 44.1991C28.4673 45.138 29.2284 45.8991 30.1672 45.8991Z"
fill="#0E3781"
/>
<path
d="M47.7334 33.9993C47.7334 35.5641 46.4649 36.8326 44.9002 36.8326C43.3354 36.8326 42.0669 35.5641 42.0669 33.9993C42.0669 32.4345 43.3354 31.166 44.9002 31.166C46.4649 31.166 47.7334 32.4345 47.7334 33.9993Z"
fill="#FFBF00"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
/>
<circle
cx="41.479"
cy="5.926"
r="3.232"
fill="#fff"
stroke="#0E3781"
stroke-linecap="round"
stroke-width="1.077"
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M44.9002 35.6993C45.839 35.6993 46.6001 34.9382 46.6001 33.9993C46.6001 33.0604 45.839 32.2993 44.9002 32.2993C43.9613 32.2993 43.2002 33.0604 43.2002 33.9993C43.2002 34.9382 43.9613 35.6993 44.9002 35.6993ZM44.9002 36.8326C46.4649 36.8326 47.7334 35.5641 47.7334 33.9993C47.7334 32.4345 46.4649 31.166 44.9002 31.166C43.3354 31.166 42.0669 32.4345 42.0669 33.9993C42.0669 35.5641 43.3354 36.8326 44.9002 36.8326Z"
fill="#0E3781"
/>
<path
d="M29.034 13.3164C29.034 14.4118 28.146 15.2997 27.0507 15.2997C25.9553 15.2997 25.0674 14.4118 25.0674 13.3164C25.0674 12.2211 25.9553 11.3331 27.0507 11.3331C28.146 11.3331 29.034 12.2211 29.034 13.3164Z"
fill="#FFBF00"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.0507 14.1664C27.5201 14.1664 27.9007 13.7859 27.9007 13.3164C27.9007 12.847 27.5201 12.4664 27.0507 12.4664C26.5812 12.4664 26.2007 12.847 26.2007 13.3164C26.2007 13.7859 26.5812 14.1664 27.0507 14.1664ZM27.0507 15.2997C28.146 15.2997 29.034 14.4118 29.034 13.3164C29.034 12.2211 28.146 11.3331 27.0507 11.3331C25.9553 11.3331 25.0674 12.2211 25.0674 13.3164C25.0674 14.4118 25.9553 15.2997 27.0507 15.2997Z"
fill="#0E3781"
/>
<path
d="M45.4669 3.96658C45.4669 6.15726 43.691 7.93316 41.5003 7.93316C39.3096 7.93316 37.5337 6.15726 37.5337 3.96658C37.5337 1.7759 39.3096 0 41.5003 0C43.691 0 45.4669 1.7759 45.4669 3.96658Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.5003 6.79985C43.065 6.79985 44.3335 5.53136 44.3335 3.96658C44.3335 2.40181 43.065 1.13331 41.5003 1.13331C39.9355 1.13331 38.667 2.40181 38.667 3.96658C38.667 5.53136 39.9355 6.79985 41.5003 6.79985ZM41.5003 7.93316C43.691 7.93316 45.4669 6.15726 45.4669 3.96658C45.4669 1.7759 43.691 0 41.5003 0C39.3096 0 37.5337 1.7759 37.5337 3.96658C37.5337 6.15726 39.3096 7.93316 41.5003 7.93316Z"
fill="#0E3781"
/>
</svg>
`;

View File

@@ -0,0 +1,23 @@
import { html } from "lit";
// This icon has static multi-colors for each theme
export function Warning() {
return html`
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 36">
<path
fill="#FFBF00"
d="M15.944 2.483c1.81-3.111 6.303-3.111 8.111 0l15.302 26.319c1.819 3.127-.438 7.049-4.055 7.049H4.698c-3.617 0-5.874-3.922-4.055-7.05L15.944 2.484Z"
/>
<path
fill="#0E3781"
fill-rule="evenodd"
d="M37.735 29.745 22.433 3.425c-1.085-1.866-3.781-1.866-4.866 0L2.265 29.746c-1.091 1.876.263 4.23 2.433 4.23h30.604c2.17 0 3.524-2.354 2.433-4.23ZM24.055 2.483c-1.808-3.111-6.302-3.111-8.11 0L.643 28.802c-1.819 3.127.438 7.049 4.055 7.049h30.604c3.617 0 5.874-3.922 4.055-7.05L24.055 2.484Z"
clip-rule="evenodd"
/>
<path
fill="#0E3781"
d="M21.876 28.345a1.876 1.876 0 1 1-3.752 0 1.876 1.876 0 0 1 3.752 0ZM17.24 11.976a.47.47 0 0 1 .467-.519h4.586c.279 0 .496.242.466.52l-1.307 12.196a.47.47 0 0 1-.466.42h-1.972a.47.47 0 0 1-.466-.42L17.24 11.976Z"
/>
</svg>
`;
}

View File

@@ -0,0 +1,41 @@
import { Meta, StoryObj } from "@storybook/web-components";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationConfirmationBody } from "../../notification/confirmation";
type Args = {
buttonText: string;
confirmationMessage: string;
handleClick: () => void;
theme: Theme;
error: string;
};
export default {
title: "Components/Notifications/Notification Confirmation Body",
argTypes: {
error: { control: "text" },
buttonText: { control: "text" },
confirmationMessage: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
},
args: {
error: "",
buttonText: "View",
confirmationMessage: "[item name] updated in Bitwarden.",
theme: ThemeTypes.Light,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
},
},
} as Meta<Args>;
const Template = (args: Args) => NotificationConfirmationBody({ ...args });
export const Default: StoryObj<Args> = {
render: Template,
};

View File

@@ -0,0 +1,54 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { Theme } from "@bitwarden/common/platform/enums";
import { themes } from "../constants/styles";
export function NotificationConfirmationMessage({
buttonText,
confirmationMessage,
handleClick,
theme,
}: {
buttonText: string;
confirmationMessage: string;
handleClick: (e: Event) => void;
theme: Theme;
}) {
return html`
<span title=${confirmationMessage} class=${notificationConfirmationMessageStyles(theme)}
>${confirmationMessage}
<a
title=${buttonText}
class=${notificationConfirmationButtonTextStyles(theme)}
@click=${handleClick}
>${buttonText}</a
></span
>
`;
}
const baseTextStyles = css`
flex-grow: 1;
overflow-x: hidden;
text-align: left;
text-overflow: ellipsis;
line-height: 24px;
white-space: nowrap;
font-family: "DM Sans", sans-serif;
font-size: 16px;
`;
const notificationConfirmationMessageStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].text.main};
font-weight: 400;
`;
const notificationConfirmationButtonTextStyles = (theme: Theme) => css`
${baseTextStyles}
color: ${themes[theme].primary[600]};
font-weight: 700;
cursor: pointer;
`;

View File

@@ -0,0 +1,58 @@
import createEmotion from "@emotion/css/create-instance";
import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { themes } from "../constants/styles";
import { PartyHorn, Warning } from "../icons";
import { NotificationConfirmationMessage } from "./confirmation-message";
export const componentClassPrefix = "notification-confirmation-body";
const { css } = createEmotion({
key: componentClassPrefix,
});
export function NotificationConfirmationBody({
buttonText,
error,
confirmationMessage,
theme = ThemeTypes.Light,
}: {
error?: string;
buttonText: string;
confirmationMessage: string;
theme: Theme;
}) {
const IconComponent = !error ? PartyHorn : Warning;
return html`
<div class=${notificationConfirmationBodyStyles({ theme })}>
<div class=${iconContainerStyles(error)}>${IconComponent({ theme })}</div>
${confirmationMessage && buttonText
? NotificationConfirmationMessage({
handleClick: () => {},
confirmationMessage,
theme,
buttonText,
})
: null}
</div>
`;
}
const iconContainerStyles = (error?: string) => css`
> svg {
width: ${!error ? "50px" : "40px"};
height: fit-content;
}
`;
const notificationConfirmationBodyStyles = ({ theme }: { theme: Theme }) => css`
gap: 16px;
display: flex;
align-items: center;
justify-content: flex-start;
background-color: ${themes[theme].background.alt};
padding: 12px;
white-space: nowrap;
`;

View File

@@ -1374,17 +1374,41 @@ export default class MainBackground {
return;
}
await this.mainContextMenuHandler?.init();
const contextMenuIsEnabled = await this.mainContextMenuHandler?.init();
if (!contextMenuIsEnabled) {
this.onUpdatedRan = this.onReplacedRan = false;
return;
}
const tab = await BrowserApi.getTabFromCurrentWindow();
if (tab) {
await this.cipherContextMenuHandler?.update(tab.url);
const currentUriIsBlocked = await firstValueFrom(
this.domainSettingsService.blockedInteractionsUris$.pipe(
map((blockedInteractionsUris) => {
if (blockedInteractionsUris && tab?.url?.length) {
const tabURL = new URL(tab.url);
const tabIsBlocked = Object.keys(blockedInteractionsUris).some((blockedHostname) =>
tabURL.hostname.endsWith(blockedHostname),
);
if (tabIsBlocked) {
return true;
}
}
return false;
}),
),
);
await this.cipherContextMenuHandler?.update(tab.url, currentUriIsBlocked);
this.onUpdatedRan = this.onReplacedRan = false;
}
}
async updateOverlayCiphers() {
// overlayBackground null in popup only contexts
// `overlayBackground` is null in popup only contexts
if (this.overlayBackground) {
await this.overlayBackground.updateOverlayCiphers();
}

View File

@@ -0,0 +1,26 @@
import { Directive, Optional } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { BitActionDirective, ButtonLikeAbstraction } from "@bitwarden/components";
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
/** Navigate the browser popup to the previous page when the component is clicked. */
@Directive({
selector: "[popupBackAction]",
standalone: true,
})
export class PopupBackBrowserDirective extends BitActionDirective {
constructor(
buttonComponent: ButtonLikeAbstraction,
private router: PopupRouterCacheService,
@Optional() validationService?: ValidationService,
@Optional() logService?: LogService,
) {
super(buttonComponent, validationService, logService);
// override `bitAction` input; the parent handles the rest
this.handler = () => this.router.back();
}
}

View File

@@ -287,7 +287,7 @@ const routes: Routes = [
path: "cipher-password-history",
component: PasswordHistoryV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
data: { elevation: 4 } satisfies RouteDataProperties,
},
{
path: "add-cipher",
@@ -310,7 +310,7 @@ const routes: Routes = [
path: "attachments",
component: AttachmentsV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
data: { elevation: 4 } satisfies RouteDataProperties,
},
{
path: "generator",
@@ -382,7 +382,7 @@ const routes: Routes = [
path: "premium",
component: PremiumV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
data: { elevation: 3 } satisfies RouteDataProperties,
},
{
path: "appearance",

View File

@@ -23,9 +23,7 @@ if (process.env.ENV === "production") {
}
function init() {
// 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
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
void platformBrowserDynamic().bootstrapModule(AppModule);
}
init();

View File

@@ -16,6 +16,9 @@
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" popupBackAction>
{{ "cancel" | i18n }}
</button>
<button
*ngIf="config?.mode !== 'add'"
type="button"

View File

@@ -29,6 +29,7 @@ import {
SendFormModule,
} from "@bitwarden/send-ui";
import { PopupBackBrowserDirective } from "../../../../platform/popup/layout/popup-back.directive";
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";
@@ -77,6 +78,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
SendFilePopoutDialogContainerComponent,
SendFormModule,
AsyncActionsModule,
PopupBackBrowserDirective,
],
})
export class SendAddEditComponent {

View File

@@ -23,5 +23,8 @@
>
{{ "exportVault" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" popupBackAction>
{{ "cancel" | i18n }}
</button>
</popup-footer>
</popup-page>

View File

@@ -7,6 +7,7 @@ import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/compo
import { ExportComponent } from "@bitwarden/vault-export-ui";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupBackBrowserDirective } from "../../../../platform/popup/layout/popup-back.directive";
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";
@@ -25,6 +26,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
PopupFooterComponent,
PopupHeaderComponent,
PopOutComponent,
PopupBackBrowserDirective,
],
})
export class ExportBrowserV2Component {

View File

@@ -46,8 +46,7 @@
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"strictTemplates": true,
"preserveWhitespaces": true
"strictTemplates": true
},
"include": [
"src",

View File

@@ -12,9 +12,7 @@ if (!ipc.platform.isDev) {
enableProdMode();
}
// 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
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
void platformBrowserDynamic().bootstrapModule(AppModule);
// Disable drag and drop to prevent malicious links from executing in the context of the app
document.addEventListener("dragover", (event) => event.preventDefault());

View File

@@ -28,7 +28,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -90,6 +90,7 @@ export class VaultComponent implements OnInit, OnDestroy {
deleted = false;
userHasPremiumAccess = false;
activeFilter: VaultFilter = new VaultFilter();
activeUserId: UserId;
private modal: ModalRef = null;
private componentIsDestroyed$ = new Subject<boolean>();
@@ -237,12 +238,12 @@ export class VaultComponent implements OnInit, OnDestroy {
});
}
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.cipherService
.failedToDecryptCiphers$(activeUserId)
.failedToDecryptCiphers$(this.activeUserId)
.pipe(
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
map((ciphers) => ciphers?.filter((c) => !c.isDeleted) ?? []),
filter((ciphers) => ciphers.length > 0),
take(1),
takeUntil(this.componentIsDestroyed$),
@@ -494,8 +495,10 @@ export class VaultComponent implements OnInit, OnDestroy {
async savedCipher(cipher: CipherView) {
this.cipherId = cipher.id;
this.action = "view";
this.go();
await this.vaultItemsComponent.refresh();
await this.cipherService.clearCache(this.activeUserId);
await this.viewComponent.load();
this.go();
}
async deletedCipher(cipher: CipherView) {

View File

@@ -45,8 +45,7 @@
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"strictTemplates": true,
"preserveWhitespaces": true
"strictTemplates": true
},
"include": ["src", "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"]
}

View File

@@ -18,6 +18,8 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
@@ -41,6 +43,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
private organizationService: OrganizationService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
configService: ConfigService,
i18nService: I18nService,
) {
super(
dialogService,
@@ -49,6 +53,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
policyService,
billingAccountProfileStateService,
accountService,
configService,
i18nService,
);
}

View File

@@ -2,5 +2,9 @@
<bit-container>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
<app-organization-plans
[enableSecretsManagerByDefault]="secretsManager"
[plan]="plan"
[productTier]="productTier"
></app-organization-plans>
</bit-container>

View File

@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild } from "@angular/core";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import { OrganizationPlansComponent } from "../../billing";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -15,29 +16,34 @@ import { SharedModule } from "../../shared";
standalone: true,
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
export class CreateOrganizationComponent {
protected secretsManager = false;
protected plan: PlanType = PlanType.Free;
protected productTier: ProductTierType = ProductTierType.Free;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.plan === "families") {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams") {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.productTier = ProductTierType.Teams;
} else if (qParams.plan === "teamsStarter") {
this.orgPlansComponent.plan = PlanType.TeamsStarter;
this.orgPlansComponent.productTier = ProductTierType.TeamsStarter;
} else if (qParams.plan === "enterprise") {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.productTier = ProductTierType.Enterprise;
constructor(private route: ActivatedRoute) {
this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => {
if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) {
this.plan = PlanType.FamiliesAnnually;
this.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams" || qParams.productTier == ProductTierType.Teams) {
this.plan = PlanType.TeamsAnnually;
this.productTier = ProductTierType.Teams;
} else if (
qParams.plan === "teamsStarter" ||
qParams.productTier == ProductTierType.TeamsStarter
) {
this.plan = PlanType.TeamsStarter;
this.productTier = ProductTierType.TeamsStarter;
} else if (
qParams.plan === "enterprise" ||
qParams.productTier == ProductTierType.Enterprise
) {
this.plan = PlanType.EnterpriseAnnually;
this.productTier = ProductTierType.Enterprise;
}
this.secretsManager = qParams.product == ProductType.SecretsManager;
});
}
}

View File

@@ -74,10 +74,10 @@ describe("WebLoginComponentService", () => {
expect(service).toBeTruthy();
});
describe("getOrgPolicies", () => {
describe("getOrgPoliciesFromOrgInvite", () => {
it("returns undefined if organization invite is null", async () => {
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.getOrgPolicies();
const result = await service.getOrgPoliciesFromOrgInvite();
expect(result).toBeUndefined();
});
@@ -94,7 +94,7 @@ describe("WebLoginComponentService", () => {
organizationName: "org-name",
});
policyApiService.getPoliciesByToken.mockRejectedValue(error);
await service.getOrgPolicies();
await service.getOrgPoliciesFromOrgInvite();
expect(logService.error).toHaveBeenCalledWith(error);
});
@@ -130,7 +130,7 @@ describe("WebLoginComponentService", () => {
of(masterPasswordPolicyOptions),
);
const result = await service.getOrgPolicies();
const result = await service.getOrgPoliciesFromOrgInvite();
expect(result).toEqual({
policies: policies,

View File

@@ -48,7 +48,7 @@ export class WebLoginComponentService
this.clientType = this.platformUtilsService.getClientType();
}
async getOrgPolicies(): Promise<PasswordPolicies | null> {
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | null> {
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
if (orgInvite != null) {

View File

@@ -1,6 +1,6 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<p bitTypography="body1">
{{ "recoverAccountTwoStepDesc" | i18n }}
{{ recoveryCodeMessage }}
<a
bitLink
href="https://bitwarden.com/help/lost-two-step-device/"

View File

@@ -1,13 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
PasswordLoginCredentials,
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@@ -16,13 +23,23 @@ import { KeyService } from "@bitwarden/key-management";
selector: "app-recover-two-factor",
templateUrl: "recover-two-factor.component.html",
})
export class RecoverTwoFactorComponent {
export class RecoverTwoFactorComponent implements OnInit {
protected formGroup = new FormGroup({
email: new FormControl(null, [Validators.required]),
masterPassword: new FormControl(null, [Validators.required]),
recoveryCode: new FormControl(null, [Validators.required]),
email: new FormControl("", [Validators.required]),
masterPassword: new FormControl("", [Validators.required]),
recoveryCode: new FormControl("", [Validators.required]),
});
/**
* Message to display to the user about the recovery code
*/
recoveryCodeMessage = "";
/**
* Whether the recovery code login feature flag is enabled
*/
private recoveryCodeLoginFeatureFlagEnabled = false;
constructor(
private router: Router,
private apiService: ApiService,
@@ -31,20 +48,35 @@ export class RecoverTwoFactorComponent {
private keyService: KeyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private toastService: ToastService,
private configService: ConfigService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private logService: LogService,
) {}
async ngOnInit() {
this.recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.RecoveryCodeLogin,
);
this.recoveryCodeMessage = this.recoveryCodeLoginFeatureFlagEnabled
? this.i18nService.t("logInBelowUsingYourSingleUseRecoveryCode")
: this.i18nService.t("recoverAccountTwoStepDesc");
}
get email(): string {
return this.formGroup.value.email;
return this.formGroup.get("email")?.value ?? "";
}
get masterPassword(): string {
return this.formGroup.value.masterPassword;
return this.formGroup.get("masterPassword")?.value ?? "";
}
get recoveryCode(): string {
return this.formGroup.value.recoveryCode;
return this.formGroup.get("recoveryCode")?.value ?? "";
}
/**
* Handles the submission of the recovery code form.
*/
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
@@ -56,12 +88,90 @@ export class RecoverTwoFactorComponent {
request.email = this.email.trim().toLowerCase();
const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key);
await this.apiService.postTwoFactorRecover(request);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("twoStepRecoverDisabled"),
});
await this.router.navigate(["/"]);
try {
await this.apiService.postTwoFactorRecover(request);
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("twoStepRecoverDisabled"),
});
if (!this.recoveryCodeLoginFeatureFlagEnabled) {
await this.router.navigate(["/"]);
return;
}
// Handle login after recovery if the feature flag is enabled
await this.handleRecoveryLogin(request);
} catch (e) {
const errorMessage = this.extractErrorMessage(e);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: errorMessage,
});
}
};
/**
* Handles the login process after a successful account recovery.
*/
private async handleRecoveryLogin(request: TwoFactorRecoveryRequest) {
// Build two-factor request to pass into PasswordLoginCredentials request using the 2FA recovery code and RecoveryCode type
const twoFactorRequest: TokenTwoFactorRequest = {
provider: TwoFactorProviderType.RecoveryCode,
token: request.recoveryCode,
remember: false,
};
const credentials = new PasswordLoginCredentials(
request.email,
this.masterPassword,
"",
twoFactorRequest,
);
try {
const authResult = await this.loginStrategyService.logIn(credentials);
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("youHaveBeenLoggedIn"),
});
await this.loginSuccessHandlerService.run(authResult.userId);
await this.router.navigate(["/settings/security/two-factor"]);
} catch (error) {
// If login errors, redirect to login page per product. Don't show error
this.logService.error("Error logging in automatically: ", (error as Error).message);
await this.router.navigate(["/login"], { queryParams: { email: request.email } });
}
}
/**
* Extracts an error message from the error object.
*/
private extractErrorMessage(error: unknown): string {
let errorMessage: string = this.i18nService.t("unexpectedError");
if (error && typeof error === "object" && "validationErrors" in error) {
const validationErrors = error.validationErrors;
if (validationErrors && typeof validationErrors === "object") {
errorMessage = Object.keys(validationErrors)
.map((key) => {
const messages = (validationErrors as Record<string, string | string[]>)[key];
return Array.isArray(messages) ? messages.join(" ") : messages;
})
.join(" ");
}
} else if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string"
) {
errorMessage = error.message;
}
return errorMessage;
}
}

View File

@@ -26,7 +26,7 @@
</p>
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<p>{{ recoveryCodeWarningMessage }}</p>
<button type="button" bitButton buttonType="secondary" (click)="recoveryCode()">
{{ "viewRecoveryCode" | i18n }}
</button>

View File

@@ -29,6 +29,9 @@ import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.s
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
@@ -52,6 +55,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
organization: Organization;
providers: any[] = [];
canAccessPremium$: Observable<boolean>;
recoveryCodeWarningMessage: string;
showPolicyWarning = false;
loading = true;
modal: ModalRef;
@@ -70,6 +74,8 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
protected policyService: PolicyService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
protected configService: ConfigService,
protected i18nService: I18nService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -79,6 +85,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
const recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.RecoveryCodeLogin,
);
this.recoveryCodeWarningMessage = recoveryCodeLoginFeatureFlagEnabled
? this.i18nService.t("yourSingleUseRecoveryCode")
: this.i18nService.t("twoStepLoginRecoveryWarning");
for (const key in TwoFactorProviders) {
// eslint-disable-next-line
if (!TwoFactorProviders.hasOwnProperty(key)) {

View File

@@ -110,6 +110,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this._plan = plan;
this.formGroup?.controls?.plan?.setValue(plan);
}
@Input() enableSecretsManagerByDefault: boolean;
private _plan = PlanType.Free;
@Input() providerId?: string;
@@ -269,6 +270,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
.subscribe(() => {
this.refreshSalesTax();
});
if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) {
this.secretsManagerSubscription.patchValue({
enabled: true,
userSeats: 1,
additionalServiceAccounts: 0,
});
}
}
ngOnDestroy() {

View File

@@ -2183,6 +2183,9 @@
"twoStepLoginRecoveryWarning": {
"message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place."
},
"yourSingleUseRecoveryCode": {
"message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place."
},
"viewRecoveryCode": {
"message": "View recovery code"
},
@@ -4193,6 +4196,9 @@
"recoverAccountTwoStepDesc": {
"message": "If you cannot access your account through your normal two-step login methods, you can use your two-step login recovery code to turn off all two-step providers on your account."
},
"logInBelowUsingYourSingleUseRecoveryCode": {
"message": "Log in below using your single-use recovery code. This will turn off all two-step providers on your account."
},
"recoverAccountTwoStep": {
"message": "Recover account two-step login"
},

View File

@@ -11,6 +11,4 @@ if (process.env.NODE_ENV === "production") {
enableProdMode();
}
// 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
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
void platformBrowserDynamic().bootstrapModule(AppModule);

View File

@@ -35,8 +35,7 @@
}
},
"angularCompilerOptions": {
"strictTemplates": true,
"preserveWhitespaces": true
"strictTemplates": true
},
"files": ["src/polyfills.ts", "src/main.ts", "src/theme.ts"],
"include": [

View File

@@ -4,7 +4,7 @@
<div class="tw-mt-4" *ngIf="!(isLoading$ | async) && !dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
<h2 class="tw-font-semibold tw-mt-4">
{{ "noAppsInOrgTitle" | i18n: organization?.name }}
</h2>
</ng-container>
@@ -13,7 +13,7 @@
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
<a class="tw-text-primary-600" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
</ng-container>
<ng-container slot="button">

View File

@@ -9,7 +9,7 @@
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
<h2 class="tw-font-semibold tw-mt-4">
{{ "noCriticalAppsTitle" | i18n }}
</h2>
</ng-container>
@@ -28,7 +28,14 @@
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-justify-between tw-mb-4">
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
<button *ngIf="isNotificationsFeatureEnabled" bitButton buttonType="primary" type="button">
<button
*ngIf="isNotificationsFeatureEnabled"
bitButton
buttonType="primary"
type="button"
[disabled]="!enableRequestPasswordChange"
(click)="requestPasswordChange()"
>
<i class="bwi bwi-envelope tw-mr-2"></i>
{{ "requestPasswordChange" | i18n }}
</button>

View File

@@ -18,7 +18,7 @@ import {
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
import {
Icons,
NoItemsModule,
@@ -27,10 +27,14 @@ import {
ToastService,
} from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { SecurityTaskType } from "@bitwarden/vault";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
import { CreateTasksRequest } from "../../vault/services/abstractions/admin-task.abstraction";
import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
@@ -38,7 +42,7 @@ import { RiskInsightsTabType } from "./risk-insights.component";
selector: "tools-critical-applications",
templateUrl: "./critical-applications.component.html",
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
providers: [],
providers: [DefaultAdminTaskService],
})
export class CriticalApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
@@ -50,6 +54,7 @@ export class CriticalApplicationsComponent implements OnInit {
protected applicationSummary = {} as ApplicationHealthReportSummary;
noItemsIcon = Icons.Security;
isNotificationsFeatureEnabled: boolean = false;
enableRequestPasswordChange = false;
async ngOnInit() {
this.isNotificationsFeatureEnabled = await this.configService.getFeatureFlag(
@@ -75,6 +80,7 @@ export class CriticalApplicationsComponent implements OnInit {
if (applications) {
this.dataSource.data = applications;
this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0;
}
});
}
@@ -109,6 +115,33 @@ export class CriticalApplicationsComponent implements OnInit {
this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname);
};
async requestPasswordChange() {
const apps = this.dataSource.data;
const cipherIds = apps
.filter((_) => _.atRiskPasswordCount > 0)
.flatMap((app) => app.atRiskMemberDetails.map((member) => member.cipherId));
const distinctCipherIds = Array.from(new Set(cipherIds));
const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({
cipherId: cipherId as CipherId,
type: SecurityTaskType.UpdateAtRiskCredential,
}));
try {
await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks);
this.toastService.showToast({
message: this.i18nService.t("notifiedMembers"),
variant: "success",
title: this.i18nService.t("success"),
});
} catch {
this.toastService.showToast({
message: this.i18nService.t("unexpectedError"),
variant: "error",
title: this.i18nService.t("error"),
});
}
}
constructor(
protected activatedRoute: ActivatedRoute,
protected router: Router,
@@ -118,6 +151,7 @@ export class CriticalApplicationsComponent implements OnInit {
protected reportService: RiskInsightsReportService,
protected i18nService: I18nService,
private configService: ConfigService,
private adminTaskService: DefaultAdminTaskService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())

View File

@@ -1,6 +1,6 @@
<div class="tw-flex-col tw-flex tw-justify-center tw-items-center tw-gap-5 tw-mt-4">
<i
class="bwi bwi-2x bwi-spinner bwi-spin text-primary"
class="bwi bwi-2x bwi-spinner bwi-spin tw-text-primary-600"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>

View File

@@ -1,6 +1,8 @@
<ng-container>
<bit-layout>
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
<div class="tw-mb-1 tw-text-primary-600" bitTypography="body1">
{{ "accessIntelligence" | i18n }}
</div>
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
<div class="tw-text-muted tw-max-w-4xl tw-mb-2">
{{ "reviewAtRiskPasswords" | i18n }}
@@ -9,7 +11,7 @@
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
>
<i
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted"
class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] tw-text-muted"
aria-hidden="true"
></i>
<span class="tw-mx-4">{{

View File

@@ -11,6 +11,4 @@ if (process.env.NODE_ENV === "production") {
enableProdMode();
}
// 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
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
void platformBrowserDynamic().bootstrapModule(AppModule);

View File

@@ -827,9 +827,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
private async generateSshKey(showNotification: boolean = true) {
await firstValueFrom(this.sdkService.client$);
const sshKey = generate_ssh_key("Ed25519");
this.cipher.sshKey.privateKey = sshKey.private_key;
this.cipher.sshKey.publicKey = sshKey.public_key;
this.cipher.sshKey.keyFingerprint = sshKey.key_fingerprint;
this.cipher.sshKey.privateKey = sshKey.privateKey;
this.cipher.sshKey.publicKey = sshKey.publicKey;
this.cipher.sshKey.keyFingerprint = sshKey.fingerprint;
if (showNotification) {
this.toastService.showToast({

View File

@@ -11,7 +11,7 @@ import {
OnInit,
Output,
} from "@angular/core";
import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { filter, firstValueFrom, map, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -80,8 +80,6 @@ export class ViewComponent implements OnDestroy, OnInit {
private previousCipherId: string;
private passwordReprompted = false;
private destroyed$ = new Subject<void>();
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
@@ -144,18 +142,14 @@ export class ViewComponent implements OnDestroy, OnInit {
async load() {
this.cleanUp();
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// Grab individual cipher from `cipherViews$` for the most up-to-date information
this.cipherService
.cipherViews$(activeUserId)
.pipe(
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.cipher = await firstValueFrom(
this.cipherService.cipherViews$(activeUserId).pipe(
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
filter((cipher) => !!cipher),
takeUntil(this.destroyed$),
)
.subscribe((cipher) => {
this.cipher = cipher;
});
),
);
this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
@@ -528,7 +522,6 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showCardNumber = false;
this.showCardCode = false;
this.passwordReprompted = false;
this.destroyed$.next();
if (this.totpInterval) {
clearInterval(this.totpInterval);
}

View File

@@ -56,13 +56,6 @@ describe("DefaultLoginComponentService", () => {
expect(service).toBeTruthy();
});
describe("getOrgPolicies", () => {
it("returns null", async () => {
const result = await service.getOrgPolicies();
expect(result).toBeNull();
});
});
describe("isLoginWithPasskeySupported", () => {
it("returns true when clientType is Web", () => {
service["clientType"] = ClientType.Web;

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LoginComponentService, PasswordPolicies } from "@bitwarden/auth/angular";
import { LoginComponentService } from "@bitwarden/auth/angular";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -23,10 +23,6 @@ export class DefaultLoginComponentService implements LoginComponentService {
protected ssoLoginService: SsoLoginServiceAbstraction,
) {}
async getOrgPolicies(): Promise<PasswordPolicies | null> {
return null;
}
isLoginWithPasskeySupported(): boolean {
return this.clientType === ClientType.Web;
}

View File

@@ -23,7 +23,7 @@ export abstract class LoginComponentService {
* Gets the organization policies if there is an organization invite.
* - Used by: Web
*/
getOrgPolicies: () => Promise<PasswordPolicies | null>;
getOrgPoliciesFromOrgInvite?: () => Promise<PasswordPolicies | null>;
/**
* Indicates whether login with passkey is supported on the given client

View File

@@ -12,6 +12,7 @@ import {
PasswordLoginCredentials,
} from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
@@ -30,6 +31,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
@@ -43,7 +45,7 @@ import {
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import { VaultIcon, WaveIcon } from "../icons";
import { LoginComponentService } from "./login-component.service";
import { LoginComponentService, PasswordPolicies } from "./login-component.service";
const BroadcasterSubscriptionId = "LoginComponent";
@@ -72,7 +74,6 @@ export class LoginComponent implements OnInit, OnDestroy {
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
private destroy$ = new Subject<void>();
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
readonly Icons = { WaveIcon, VaultIcon };
clientType: ClientType;
@@ -97,11 +98,6 @@ export class LoginComponent implements OnInit, OnDestroy {
return this.formGroup.controls.email;
}
// Web properties
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
policies: Policy[] | undefined;
showResetPasswordAutoEnrollWarning = false;
// Desktop properties
deferFocus: boolean | null = null;
@@ -281,18 +277,39 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}
// User logged in successfully so execute side effects
await this.loginSuccessHandlerService.run(authResult.userId);
this.loginEmailService.clearValues();
// Determine where to send the user next
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
this.loginEmailService.clearValues();
await this.router.navigate(["update-temp-password"]);
return;
}
// If none of the above cases are true, proceed with login...
await this.evaluatePassword();
// TODO: PM-18269 - evaluate if we can combine this with the
// password evaluation done in the password login strategy.
// If there's an existing org invite, use it to get the org's password policies
// so we can evaluate the MP against the org policies
if (this.loginComponentService.getOrgPoliciesFromOrgInvite) {
const orgPolicies: PasswordPolicies | null =
await this.loginComponentService.getOrgPoliciesFromOrgInvite();
this.loginEmailService.clearValues();
if (orgPolicies) {
// Since we have retrieved the policies, we can go ahead and set them into state for future use
// e.g., the update-password page currently only references state for policy data and
// doesn't fallback to pulling them from the server like it should if they are null.
await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies);
const isPasswordChangeRequired = await this.isPasswordChangeRequiredByOrgPolicy(
orgPolicies.enforcedPasswordPolicyOptions,
);
if (isPasswordChangeRequired) {
await this.router.navigate(["update-password"]);
return;
}
}
}
if (this.clientType === ClientType.Browser) {
await this.router.navigate(["/tabs/vault"]);
@@ -310,54 +327,51 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
}
protected async evaluatePassword(): Promise<void> {
/**
* Checks if the master password meets the enforced policy requirements
* and if the user is required to change their password.
*/
private async isPasswordChangeRequiredByOrgPolicy(
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions,
): Promise<boolean> {
try {
// If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) {
this.enforcedMasterPasswordOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(),
);
if (enforcedPasswordPolicyOptions == undefined) {
return false;
}
if (this.requirePasswordChange()) {
await this.router.navigate(["update-password"]);
return;
// Note: we deliberately do not check enforcedPasswordPolicyOptions.enforceOnLogin
// as existing users who are logging in after getting an org invite should
// always be forced to set a password that meets the org's policy.
// Org Invite -> Registration also works this way for new BW users as well.
const masterPassword = this.formGroup.controls.masterPassword.value;
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
if (!masterPassword) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.formGroup.value.email ?? undefined,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
enforcedPasswordPolicyOptions,
);
} catch (e) {
// Do not prevent unlock if there is an error evaluating policies
this.logService.error(e);
return false;
}
}
/**
* Checks if the master password meets the enforced policy requirements
* If not, returns false
*/
private requirePasswordChange(): boolean {
if (
this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin
) {
return false;
}
const masterPassword = this.formGroup.controls.masterPassword.value;
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
if (!masterPassword) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.formGroup.value.email ?? undefined,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
this.enforcedMasterPasswordOptions,
);
private async setPoliciesIntoState(userId: UserId, policies: Policy[]): Promise<void> {
const policiesData: { [id: string]: PolicyData } = {};
policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
await this.policyService.replace(policiesData, userId);
}
protected async startAuthRequestLogin(): Promise<void> {
@@ -528,12 +542,6 @@ export class LoginComponent implements OnInit, OnDestroy {
}
private async defaultOnInit(): Promise<void> {
// If there's an existing org invite, use it to get the password policies
const orgPolicies = await this.loginComponentService.getOrgPolicies();
this.policies = orgPolicies?.policies;
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false;
let paramEmailIsSet = false;
const params = await firstValueFrom(this.activatedRoute.queryParams);

View File

@@ -7,4 +7,5 @@ export enum TwoFactorProviderType {
Remember = 5,
OrganizationDuo = 6,
WebAuthn = 7,
RecoveryCode = 8,
}

View File

@@ -50,6 +50,7 @@ export enum FeatureFlag {
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
NewDeviceVerification = "new-device-verification",
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
RecoveryCodeLogin = "pm-17128-recovery-code-login",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -110,6 +111,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
[FeatureFlag.NewDeviceVerification]: FALSE,
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
[FeatureFlag.RecoveryCodeLogin]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -65,7 +65,6 @@ export default class Domain {
key: SymmetricCryptoKey = null,
objectContext: string = "No Domain Context",
): Promise<T> {
const promises = [];
const self: any = this;
for (const prop in map) {
@@ -74,27 +73,15 @@ export default class Domain {
continue;
}
(function (theProp) {
const p = Promise.resolve()
.then(() => {
const mapProp = map[theProp] || theProp;
if (self[mapProp]) {
return self[mapProp].decrypt(
orgId,
key,
`Property: ${prop}; ObjectContext: ${objectContext}`,
);
}
return null;
})
.then((val: any) => {
(viewModel as any)[theProp] = val;
});
promises.push(p);
})(prop);
const mapProp = map[prop] || prop;
if (self[mapProp]) {
(viewModel as any)[prop] = await self[mapProp].decrypt(
orgId,
key,
`Property: ${prop}; ObjectContext: ${objectContext}`,
);
}
}
await Promise.all(promises);
return viewModel;
}
@@ -121,22 +108,20 @@ export default class Domain {
_: Constructor<TThis> = this.constructor as Constructor<TThis>,
objectContext: string = "No Domain Context",
): Promise<DecryptedObject<TThis, TEncryptedKeys>> {
const promises = [];
const decryptedObjects = [];
for (const prop of encryptedProperties) {
const value = (this as any)[prop] as EncString;
promises.push(
this.decryptProperty(
prop,
value,
key,
encryptService,
`Property: ${prop.toString()}; ObjectContext: ${objectContext}`,
),
const decrypted = await this.decryptProperty(
prop,
value,
key,
encryptService,
`Property: ${prop.toString()}; ObjectContext: ${objectContext}`,
);
decryptedObjects.push(decrypted);
}
const decryptedObjects = await Promise.all(promises);
const decryptedObject = decryptedObjects.reduce(
(acc, obj) => {
return { ...acc, ...obj };

View File

@@ -12,7 +12,10 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../data/cipher.data";
import { LocalData } from "../data/local.data";
import { AttachmentView } from "../view/attachment.view";
import { CipherView } from "../view/cipher.view";
import { FieldView } from "../view/field.view";
import { PasswordHistoryView } from "../view/password-history.view";
import { Attachment } from "./attachment";
import { Card } from "./card";
@@ -136,6 +139,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.key != null) {
const encryptService = Utils.getContainerService().getEncryptService();
const keyBytes = await encryptService.decryptToBytes(
this.key,
encKey,
@@ -198,44 +202,28 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
}
if (this.attachments != null && this.attachments.length > 0) {
const attachments: any[] = [];
await this.attachments.reduce((promise, attachment) => {
return promise
.then(() => {
return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey);
})
.then((decAttachment) => {
attachments.push(decAttachment);
});
}, Promise.resolve());
const attachments: AttachmentView[] = [];
for (const attachment of this.attachments) {
attachments.push(
await attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey),
);
}
model.attachments = attachments;
}
if (this.fields != null && this.fields.length > 0) {
const fields: any[] = [];
await this.fields.reduce((promise, field) => {
return promise
.then(() => {
return field.decrypt(this.organizationId, encKey);
})
.then((decField) => {
fields.push(decField);
});
}, Promise.resolve());
const fields: FieldView[] = [];
for (const field of this.fields) {
fields.push(await field.decrypt(this.organizationId, encKey));
}
model.fields = fields;
}
if (this.passwordHistory != null && this.passwordHistory.length > 0) {
const passwordHistory: any[] = [];
await this.passwordHistory.reduce((promise, ph) => {
return promise
.then(() => {
return ph.decrypt(this.organizationId, encKey);
})
.then((decPh) => {
passwordHistory.push(decPh);
});
}, Promise.resolve());
const passwordHistory: PasswordHistoryView[] = [];
for (const ph of this.passwordHistory) {
passwordHistory.push(await ph.decrypt(this.organizationId, encKey));
}
model.passwordHistory = passwordHistory;
}

View File

@@ -45,7 +45,6 @@ export type ChipSelectOption<T> = Option<T> & {
multi: true,
},
],
preserveWhitespaces: false,
})
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
@ViewChild(MenuComponent) menu: MenuComponent;

View File

@@ -22,7 +22,6 @@ enum CharacterType {
}
</span>
}`,
preserveWhitespaces: false,
standalone: true,
})
export class ColorPasswordComponent {

View File

@@ -9,10 +9,7 @@ import { ButtonModule } from "../button";
import { CalloutModule } from "../callout";
import { LayoutComponent } from "../layout";
import { mockLayoutI18n } from "../layout/mocks";
import {
disableBothThemeDecorator,
positionFixedWrapperDecorator,
} from "../stories/storybook-decorators";
import { positionFixedWrapperDecorator } from "../stories/storybook-decorators";
import { TypographyModule } from "../typography";
import { I18nMockService } from "../utils";
@@ -30,7 +27,6 @@ export default {
},
decorators: [
positionFixedWrapperDecorator(),
disableBothThemeDecorator,
moduleMetadata({
imports: [
RouterTestingModule,

View File

@@ -1,4 +1,4 @@
export { ButtonType } from "./shared/button-like.abstraction";
export { ButtonType, ButtonLikeAbstraction } from "./shared/button-like.abstraction";
export * from "./a11y";
export * from "./async-actions";
export * from "./avatar";

View File

@@ -29,7 +29,6 @@ import { SideNavService } from "./side-nav.service";
],
standalone: true,
imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe],
preserveWhitespaces: false,
})
export class NavGroupComponent extends NavBaseComponent implements AfterContentInit {
@ContentChildren(NavBaseComponent, {

View File

@@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { DialogService } from "../../dialog";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { disableBothThemeDecorator, positionFixedWrapperDecorator } from "../storybook-decorators";
import { positionFixedWrapperDecorator } from "../storybook-decorators";
import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component";
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
@@ -31,7 +31,6 @@ export default {
component: LayoutComponent,
decorators: [
positionFixedWrapperDecorator(),
disableBothThemeDecorator,
moduleMetadata({
imports: [
KitchenSinkSharedModule,

View File

@@ -17,15 +17,3 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin
${wrapper ? wrapper(story) : story}
</div>`,
);
export const disableBothThemeDecorator = componentWrapperDecorator(
(story) => story,
({ globals }) => {
/**
* avoid a bug with the way that we render the same component twice in the same iframe and how
* that interacts with the router-outlet
*/
const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"];
return { theme: themeOverride };
},
);

View File

@@ -23,7 +23,6 @@ import { ToastComponent } from "./toast.component";
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
]),
],
preserveWhitespaces: false,
standalone: true,
imports: [ToastComponent],
})

View File

@@ -12,7 +12,6 @@ let nextId = 0;
@Component({
selector: "bit-toggle-group",
templateUrl: "./toggle-group.component.html",
preserveWhitespaces: false,
standalone: true,
})
export class ToggleGroupComponent<TValue = unknown> {

View File

@@ -19,7 +19,6 @@ let nextId = 0;
@Component({
selector: "bit-toggle",
templateUrl: "./toggle.component.html",
preserveWhitespaces: false,
standalone: true,
imports: [NgClass],
})

View File

@@ -194,6 +194,9 @@
--tw-ring-offset-color: #002b36;
}
/** Used by CDK a11y services */
@import "@angular/cdk/a11y-prebuilt.css";
@import "./popover/popover.component.css";
@import "./search/search.component.css";

View File

@@ -368,20 +368,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => {
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
for (const org of orgs) {
encOrgKeyData[org.id] = {
type: "organization",
key: org.key,
};
});
}
providerOrgs.forEach((org) => {
for (const org of providerOrgs) {
encOrgKeyData[org.id] = {
type: "provider",
providerId: org.providerId,
key: org.key,
};
});
}
return encOrgKeyData;
});
}

View File

@@ -42,7 +42,6 @@ import { EventType } from "@bitwarden/common/enums";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
@@ -56,7 +55,8 @@ import {
SelectModule,
ToastService,
} from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
@@ -81,6 +81,7 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
ExportScopeCalloutComponent,
UserVerificationDialogComponent,
PasswordStrengthV2Component,
GeneratorServicesModule,
],
})
export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
@@ -175,14 +176,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
private destroy$ = new Subject<void>();
private onlyManagedCollections = true;
private onGenerate$ = new Subject<GenerateRequest>();
constructor(
protected i18nService: I18nService,
protected toastService: ToastService,
protected exportService: VaultExportServiceAbstraction,
protected eventCollectionService: EventCollectionService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
protected generatorService: CredentialGeneratorService,
private policyService: PolicyService,
private logService: LogService,
private formBuilder: UntypedFormBuilder,
@@ -218,6 +219,17 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
// Wire up the password generation for the password-protected export
this.generatorService
.generate$(Generators.password, { on$: this.onGenerate$ })
.pipe(takeUntil(this.destroy$))
.subscribe((generated) => {
this.exportForm.patchValue({
filePassword: generated.credential,
confirmFilePassword: generated.credential,
});
});
if (this.organizationId) {
this.organizations$ = this.organizationService
.memberOrganizations$(userId)
@@ -302,10 +314,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
}
generatePassword = async () => {
const [options] = await this.passwordGenerationService.getOptions();
const generatedPassword = await this.passwordGenerationService.generatePassword(options);
this.exportForm.get("filePassword").setValue(generatedPassword);
this.exportForm.get("confirmFilePassword").setValue(generatedPassword);
this.onGenerate$.next({ source: "export" });
};
submit = async () => {

View File

@@ -9,6 +9,7 @@
"@bitwarden/common/*": ["../../../../common/src/*"],
"@bitwarden/components": ["../../../../components/src"],
"@bitwarden/generator-core": ["../../../../tools/generator/core/src"],
"@bitwarden/generator-components": ["../../../../tools/generator/components/src"],
"@bitwarden/generator-history": ["../../../../tools/generator/extensions/history/src"],
"@bitwarden/generator-legacy": ["../../../../tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../../../tools/generator/extensions/navigation/src"],

View File

@@ -104,9 +104,9 @@ export class SshKeySectionComponent implements OnInit {
await firstValueFrom(this.sdkService.client$);
const sshKey = generate_ssh_key("Ed25519");
this.sshKeyForm.setValue({
privateKey: sshKey.private_key,
publicKey: sshKey.public_key,
keyFingerprint: sshKey.key_fingerprint,
privateKey: sshKey.privateKey,
publicKey: sshKey.publicKey,
keyFingerprint: sshKey.fingerprint,
});
}
}

27
package-lock.json generated
View File

@@ -24,7 +24,7 @@
"@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13",
"@bitwarden/sdk-internal": "0.2.0-main.38",
"@bitwarden/sdk-internal": "0.2.0-main.105",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "3.0.2",
@@ -91,6 +91,7 @@
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
"@storybook/addon-themes": "^8.5.2",
"@storybook/angular": "8.5.2",
"@storybook/manager-api": "8.5.2",
"@storybook/theming": "8.5.2",
@@ -153,6 +154,7 @@
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "14.1.1",
"json5": "2.2.3",
"lint-staged": "15.4.1",
"mini-css-extract-plugin": "2.9.2",
"node-ipc": "9.2.1",
@@ -4469,9 +4471,9 @@
"link": true
},
"node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.38",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz",
"integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==",
"version": "0.2.0-main.105",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.105.tgz",
"integrity": "sha512-MaQFJbuKTCbN9oZC/+opYVeegaNNJpiUv9/zx+gu8KxWmX0hyEkNPtHKxBjDt3kLLz69CudDtUxEgqOfcDsYAw==",
"license": "GPL-3.0"
},
"node_modules/@bitwarden/vault": {
@@ -8712,6 +8714,22 @@
"storybook": "^8.5.2"
}
},
"node_modules/@storybook/addon-themes": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-themes/-/addon-themes-8.5.2.tgz",
"integrity": "sha512-MTJkPwXqLK2Co186EUw2wr+1CpVRMbuWsOmQvhMHeU704kQtSYKkhu/xmaExuDYMupn5xiKG0p8Pt5Ck3fEObQ==",
"dev": true,
"dependencies": {
"ts-dedent": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^8.5.2"
}
},
"node_modules/@storybook/addon-toolbars": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.5.2.tgz",
@@ -22127,7 +22145,6 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},

View File

@@ -52,6 +52,7 @@
"@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.2",
"@storybook/addon-themes": "8.5.2",
"@storybook/angular": "8.5.2",
"@storybook/manager-api": "8.5.2",
"@storybook/theming": "8.5.2",
@@ -114,6 +115,7 @@
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "14.1.1",
"json5": "2.2.3",
"lint-staged": "15.4.1",
"mini-css-extract-plugin": "2.9.2",
"node-ipc": "9.2.1",
@@ -154,7 +156,7 @@
"@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13",
"@bitwarden/sdk-internal": "0.2.0-main.38",
"@bitwarden/sdk-internal": "0.2.0-main.105",
"@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5",
"@koa/multer": "3.0.2",

View File

@@ -5,8 +5,10 @@
import fs from "fs";
import path from "path";
const renovateConfig = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json"), "utf8"),
import JSON5 from "json5";
const renovateConfig = JSON5.parse(
fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json5"), "utf8"),
);
const packagesWithOwners = renovateConfig.packageRules