mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
Merge branch 'PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build' of github.com:bitwarden/clients into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build
merge main
This commit is contained in:
@@ -585,6 +585,9 @@
|
|||||||
"archiveItemConfirmDesc": {
|
"archiveItemConfirmDesc": {
|
||||||
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||||
},
|
},
|
||||||
|
"upgradeToUseArchive": {
|
||||||
|
"message": "A premium membership is required to use Archive."
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"message": "Edit"
|
"message": "Edit"
|
||||||
},
|
},
|
||||||
@@ -594,6 +597,9 @@
|
|||||||
"viewAll": {
|
"viewAll": {
|
||||||
"message": "View all"
|
"message": "View all"
|
||||||
},
|
},
|
||||||
|
"showAll": {
|
||||||
|
"message": "Show all"
|
||||||
|
},
|
||||||
"viewLess": {
|
"viewLess": {
|
||||||
"message": "View less"
|
"message": "View less"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1344,7 +1344,7 @@ export default class NotificationBackground {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
const extensionUrl = BrowserApi.getRuntimeURL("popup/index.html");
|
||||||
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
|
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
|
||||||
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
|
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2949,13 +2949,13 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
|
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
|
||||||
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
|
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
|
||||||
|
|
||||||
const iframeUrl = chrome.runtime.getURL(
|
const iframeUrl = BrowserApi.getRuntimeURL(
|
||||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
|
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
|
||||||
);
|
);
|
||||||
const styleSheetUrl = chrome.runtime.getURL(
|
const styleSheetUrl = BrowserApi.getRuntimeURL(
|
||||||
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
|
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
|
||||||
);
|
);
|
||||||
const extensionOrigin = new URL(iframeUrl).origin;
|
const extensionOrigin = iframeUrl ? new URL(iframeUrl).origin : null;
|
||||||
|
|
||||||
this.postMessageToPort(port, {
|
this.postMessageToPort(port, {
|
||||||
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
|
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ describe("ContentMessageHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends an authResult message", () => {
|
it("sends an authResult message", () => {
|
||||||
postWindowMessage({ command: "authResult", lastpass: true, code: "code", state: "state" });
|
postWindowMessage(
|
||||||
|
{ command: "authResult", lastpass: true, code: "code", state: "state" },
|
||||||
|
"https://localhost/",
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: "authResult",
|
command: "authResult",
|
||||||
@@ -68,7 +72,11 @@ describe("ContentMessageHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends a webAuthnResult message", () => {
|
it("sends a webAuthnResult message", () => {
|
||||||
postWindowMessage({ command: "webAuthnResult", data: "data", remember: true });
|
postWindowMessage(
|
||||||
|
{ command: "webAuthnResult", data: "data", remember: true },
|
||||||
|
"https://localhost/",
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: "webAuthnResult",
|
command: "webAuthnResult",
|
||||||
@@ -82,7 +90,7 @@ describe("ContentMessageHandler", () => {
|
|||||||
const mockCode = "mockCode";
|
const mockCode = "mockCode";
|
||||||
const command = "duoResult";
|
const command = "duoResult";
|
||||||
|
|
||||||
postWindowMessage({ command: command, code: mockCode });
|
postWindowMessage({ command: command, code: mockCode }, "https://localhost/", window);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||||
command: command,
|
command: command,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type NotificationBarWindowMessage = {
|
|||||||
};
|
};
|
||||||
error?: string;
|
error?: string;
|
||||||
initData?: NotificationBarIframeInitData;
|
initData?: NotificationBarIframeInitData;
|
||||||
|
parentOrigin?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationBarWindowMessageHandlers = {
|
type NotificationBarWindowMessageHandlers = {
|
||||||
|
|||||||
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
121
apps/browser/src/autofill/notification/bar.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { postWindowMessage } from "../spec/testing-utils";
|
||||||
|
|
||||||
|
import { NotificationBarWindowMessage } from "./abstractions/notification-bar";
|
||||||
|
import "./bar";
|
||||||
|
|
||||||
|
jest.mock("lit", () => ({ render: jest.fn() }));
|
||||||
|
jest.mock("@lit-labs/signals", () => ({
|
||||||
|
signal: jest.fn((testValue) => ({ get: (): typeof testValue => testValue })),
|
||||||
|
}));
|
||||||
|
jest.mock("../content/components/notification/container", () => ({
|
||||||
|
NotificationContainer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("NotificationBar iframe handleWindowMessage security", () => {
|
||||||
|
const trustedOrigin = "http://localhost";
|
||||||
|
const maliciousOrigin = "https://malicious.com";
|
||||||
|
|
||||||
|
const createMessage = (
|
||||||
|
overrides: Partial<NotificationBarWindowMessage> = {},
|
||||||
|
): NotificationBarWindowMessage => ({
|
||||||
|
command: "initNotificationBar",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Object.defineProperty(globalThis, "location", {
|
||||||
|
value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` },
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
Object.defineProperty(globalThis, "parent", {
|
||||||
|
value: mock<Window>(),
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
globalThis.dispatchEvent(new Event("load"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
description: "not from parent window",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => mock<Window>(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with mismatched origin",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: maliciousOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "without command field",
|
||||||
|
message: () => ({}),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "initNotificationBar with mismatched parentOrigin",
|
||||||
|
message: () => createMessage({ parentOrigin: maliciousOrigin }),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "when windowMessageOrigin is not set",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: "different-origin",
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
resetOrigin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with null source",
|
||||||
|
message: () => createMessage(),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: (): null => null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "with unknown command",
|
||||||
|
message: () => createMessage({ command: "unknownCommand" }),
|
||||||
|
origin: trustedOrigin,
|
||||||
|
source: () => globalThis.parent,
|
||||||
|
},
|
||||||
|
])("should reject messages $description", ({ message, origin, source, resetOrigin }) => {
|
||||||
|
if (resetOrigin) {
|
||||||
|
Object.defineProperty(globalThis, "location", {
|
||||||
|
value: { search: "" },
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||||
|
postWindowMessage(message(), origin, source());
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept and handle valid trusted messages", () => {
|
||||||
|
const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation();
|
||||||
|
spy.mockClear();
|
||||||
|
|
||||||
|
const validMessage = createMessage({
|
||||||
|
parentOrigin: trustedOrigin,
|
||||||
|
initData: {
|
||||||
|
type: "change",
|
||||||
|
isVaultLocked: false,
|
||||||
|
removeIndividualVault: false,
|
||||||
|
importType: null,
|
||||||
|
launchTimestamp: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
postWindowMessage(validMessage, trustedOrigin, globalThis.parent);
|
||||||
|
expect(validMessage.command).toBe("initNotificationBar");
|
||||||
|
expect(validMessage.parentOrigin).toBe(trustedOrigin);
|
||||||
|
expect(validMessage.initData).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,6 +24,13 @@ import {
|
|||||||
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
|
||||||
let windowMessageOrigin: string;
|
let windowMessageOrigin: string;
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
const trustedParentOrigin = urlParams.get("parentOrigin");
|
||||||
|
|
||||||
|
if (trustedParentOrigin) {
|
||||||
|
windowMessageOrigin = trustedParentOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
|
||||||
initNotificationBar: ({ message }) => initNotificationBar(message),
|
initNotificationBar: ({ message }) => initNotificationBar(message),
|
||||||
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message),
|
||||||
@@ -395,15 +402,27 @@ function setupWindowMessageListener() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowMessage(event: MessageEvent) {
|
function handleWindowMessage(event: MessageEvent) {
|
||||||
if (!windowMessageOrigin) {
|
if (event?.source !== globalThis.parent) {
|
||||||
windowMessageOrigin = event.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.origin !== windowMessageOrigin) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = event.data as NotificationBarWindowMessage;
|
const message = event.data as NotificationBarWindowMessage;
|
||||||
|
if (!message?.command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!windowMessageOrigin || event.origin !== windowMessageOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.command === "initNotificationBar" &&
|
||||||
|
message.parentOrigin &&
|
||||||
|
message.parentOrigin !== event.origin
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handler = notificationBarWindowMessageHandlers[message.command];
|
const handler = notificationBarWindowMessageHandlers[message.command];
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return;
|
||||||
@@ -431,5 +450,8 @@ function getResolvedTheme(theme: Theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function postMessageToParent(message: NotificationBarWindowMessage) {
|
function postMessageToParent(message: NotificationBarWindowMessage) {
|
||||||
globalThis.parent.postMessage(message, windowMessageOrigin || "*");
|
if (!windowMessageOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
globalThis.parent.postMessage(message, windowMessageOrigin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type InitAutofillInlineMenuButtonMessage = UpdateAuthStatusMessage & {
|
|||||||
styleSheetUrl: string;
|
styleSheetUrl: string;
|
||||||
translations: Record<string, string>;
|
translations: Record<string, string>;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutofillInlineMenuButtonWindowMessageHandlers = {
|
export type AutofillInlineMenuButtonWindowMessageHandlers = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { InlineMenuCipherData } from "../../../background/abstractions/overlay.b
|
|||||||
export type AutofillInlineMenuContainerMessage = {
|
export type AutofillInlineMenuContainerMessage = {
|
||||||
command: string;
|
command: string;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
token?: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & {
|
export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage &
|
|||||||
showInlineMenuAccountCreation?: boolean;
|
showInlineMenuAccountCreation?: boolean;
|
||||||
showPasskeysLabels?: boolean;
|
showPasskeysLabels?: boolean;
|
||||||
portKey: string;
|
portKey: string;
|
||||||
|
token: string;
|
||||||
generatedPassword?: string;
|
generatedPassword?: string;
|
||||||
showSaveLoginMenu?: boolean;
|
showSaveLoginMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => {
|
||||||
@@ -217,7 +217,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey);
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
expect(updateElementStylesSpy).not.toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
autofillInlineMenuIframeService["iframe"].contentWindow.postMessage,
|
||||||
).toHaveBeenCalledWith(message, "*");
|
).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets a light theme based on the user's system preferences", () => {
|
it("sets a light theme based on the user's system preferences", () => {
|
||||||
@@ -262,7 +262,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Light,
|
theme: ThemeType.Light,
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "initAutofillInlineMenuList",
|
command: "initAutofillInlineMenuList",
|
||||||
theme: ThemeType.Dark,
|
theme: ThemeType.Dark,
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ describe("AutofillInlineMenuIframeService", () => {
|
|||||||
command: "updateAutofillInlineMenuColorScheme",
|
command: "updateAutofillInlineMenuColorScheme",
|
||||||
colorScheme: "normal",
|
colorScheme: "normal",
|
||||||
},
|
},
|
||||||
"*",
|
autofillInlineMenuIframeService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
||||||
import {
|
import {
|
||||||
BackgroundPortMessageHandlers,
|
BackgroundPortMessageHandlers,
|
||||||
@@ -15,6 +16,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
private readonly sendExtensionMessage = sendExtensionMessage;
|
private readonly sendExtensionMessage = sendExtensionMessage;
|
||||||
private port: chrome.runtime.Port | null = null;
|
private port: chrome.runtime.Port | null = null;
|
||||||
private portKey: string;
|
private portKey: string;
|
||||||
|
private readonly extensionOrigin: string;
|
||||||
private iframeMutationObserver: MutationObserver;
|
private iframeMutationObserver: MutationObserver;
|
||||||
private iframe: HTMLIFrameElement;
|
private iframe: HTMLIFrameElement;
|
||||||
private ariaAlertElement: HTMLDivElement;
|
private ariaAlertElement: HTMLDivElement;
|
||||||
@@ -69,6 +71,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
private iframeTitle: string,
|
private iframeTitle: string,
|
||||||
private ariaAlert?: string,
|
private ariaAlert?: string,
|
||||||
) {
|
) {
|
||||||
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
this.iframeMutationObserver = new MutationObserver(this.handleMutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
* that is declared.
|
* that is declared.
|
||||||
*/
|
*/
|
||||||
initMenuIframe() {
|
initMenuIframe() {
|
||||||
this.defaultIframeAttributes.src = chrome.runtime.getURL("overlay/menu.html");
|
this.defaultIframeAttributes.src = BrowserApi.getRuntimeURL("overlay/menu.html");
|
||||||
this.defaultIframeAttributes.title = this.iframeTitle;
|
this.defaultIframeAttributes.title = this.iframeTitle;
|
||||||
|
|
||||||
this.iframe = globalThis.document.createElement("iframe");
|
this.iframe = globalThis.document.createElement("iframe");
|
||||||
@@ -259,7 +262,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private postMessageToIFrame(message: any) {
|
private postMessageToIFrame(message: any) {
|
||||||
this.iframe.contentWindow?.postMessage({ portKey: this.portKey, ...message }, "*");
|
this.iframe.contentWindow?.postMessage(
|
||||||
|
{ portKey: this.portKey, ...message },
|
||||||
|
this.extensionOrigin,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks";
|
import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks";
|
||||||
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
|
||||||
|
|
||||||
@@ -10,11 +11,11 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
|
|
||||||
let autofillInlineMenuButton: AutofillInlineMenuButton;
|
let autofillInlineMenuButton: AutofillInlineMenuButton;
|
||||||
const portKey: string = "inlineMenuButtonPortKey";
|
const portKey: string = "inlineMenuButtonPortKey";
|
||||||
|
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.body.innerHTML = `<autofill-inline-menu-button></autofill-inline-menu-button>`;
|
document.body.innerHTML = `<autofill-inline-menu-button></autofill-inline-menu-button>`;
|
||||||
autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button");
|
autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button");
|
||||||
autofillInlineMenuButton["messageOrigin"] = "https://localhost/";
|
|
||||||
jest.spyOn(globalThis.document, "createElement");
|
jest.spyOn(globalThis.document, "createElement");
|
||||||
jest.spyOn(globalThis.parent, "postMessage");
|
jest.spyOn(globalThis.parent, "postMessage");
|
||||||
});
|
});
|
||||||
@@ -56,8 +57,8 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
autofillInlineMenuButton["buttonElement"].click();
|
autofillInlineMenuButton["buttonElement"].click();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "autofillInlineMenuButtonClicked", portKey },
|
{ command: "autofillInlineMenuButtonClicked", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -70,7 +71,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => {
|
it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||||
@@ -84,7 +85,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({
|
||||||
@@ -98,7 +99,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
.mockReturnValue(autofillInlineMenuButton["buttonElement"]);
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
@@ -113,12 +114,12 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
|
||||||
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey },
|
{ command: "triggerDelayedAutofillInlineMenuClosure", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuButtonAuthStatus",
|
command: "updateAutofillInlineMenuButtonAuthStatus",
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -143,6 +145,7 @@ describe("AutofillInlineMenuButton", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuColorScheme",
|
command: "updateAutofillInlineMenuColorScheme",
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background";
|
||||||
import {
|
import {
|
||||||
createAutofillOverlayCipherDataMock,
|
createAutofillOverlayCipherDataMock,
|
||||||
@@ -23,6 +24,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
||||||
const portKey: string = "inlineMenuListPortKey";
|
const portKey: string = "inlineMenuListPortKey";
|
||||||
|
const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id";
|
||||||
const events: { eventName: any; callback: any }[] = [];
|
const events: { eventName: any; callback: any }[] = [];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -67,8 +69,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
unlockButton.dispatchEvent(new Event("click"));
|
unlockButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "unlockVault", portKey },
|
{ command: "unlockVault", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -134,8 +136,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
addVaultItemButton.dispatchEvent(new Event("click"));
|
addVaultItemButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
{
|
||||||
"*",
|
command: "addNewVaultItem",
|
||||||
|
portKey,
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -324,8 +331,9 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
inlineMenuCipherId: "1",
|
inlineMenuCipherId: "1",
|
||||||
usePasskey: false,
|
usePasskey: false,
|
||||||
portKey,
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
},
|
},
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -492,8 +500,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
viewCipherButton.dispatchEvent(new Event("click"));
|
viewCipherButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "viewSelectedCipher", inlineMenuCipherId: "1", portKey },
|
{
|
||||||
"*",
|
command: "viewSelectedCipher",
|
||||||
|
inlineMenuCipherId: "1",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -581,8 +594,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
|
newVaultItemButtonSpy.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login },
|
{
|
||||||
"*",
|
command: "addNewVaultItem",
|
||||||
|
portKey,
|
||||||
|
addNewCipherType: CipherType.Login,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -826,8 +844,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
fillGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -843,7 +861,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -857,8 +875,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "fillGeneratedPassword", portKey },
|
{ command: "fillGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -896,8 +914,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
refreshGeneratedPasswordButton.dispatchEvent(new Event("click"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -913,7 +931,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -927,8 +945,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "refreshGeneratedPassword", portKey },
|
{ command: "refreshGeneratedPassword", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -972,7 +990,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
|
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
|
||||||
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -983,7 +1001,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -994,7 +1012,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
.mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]);
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
globalThis.document.dispatchEvent(new MouseEvent("mouseout"));
|
||||||
@@ -1010,11 +1028,11 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
.spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector")
|
||||||
.mockReturnValue(null);
|
.mockReturnValue(null);
|
||||||
|
|
||||||
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
|
postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" });
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "checkAutofillInlineMenuButtonFocused", portKey },
|
{ command: "checkAutofillInlineMenuButtonFocused", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1022,7 +1040,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
|
const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
|
||||||
|
|
||||||
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" });
|
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers", token: "test-token" });
|
||||||
|
|
||||||
expect(updateCiphersSpy).toHaveBeenCalled();
|
expect(updateCiphersSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1062,7 +1080,10 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword" });
|
postWindowMessage({
|
||||||
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
|
token: "test-token",
|
||||||
|
});
|
||||||
|
|
||||||
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1074,6 +1095,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
generatedPassword,
|
generatedPassword,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalled();
|
||||||
@@ -1090,6 +1112,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage({
|
postWindowMessage({
|
||||||
command: "updateAutofillInlineMenuGeneratedPassword",
|
command: "updateAutofillInlineMenuGeneratedPassword",
|
||||||
generatedPassword,
|
generatedPassword,
|
||||||
|
token: "test-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1);
|
||||||
@@ -1115,7 +1138,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
|
expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1124,7 +1147,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
postWindowMessage(createInitAutofillInlineMenuListMessageMock());
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
postWindowMessage({ command: "showSaveLoginInlineMenuList" });
|
postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
|
expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1143,7 +1166,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
"setAttribute",
|
"setAttribute",
|
||||||
);
|
);
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
||||||
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
||||||
@@ -1161,7 +1184,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
|
||||||
jest.spyOn(unlockButton as HTMLElement, "focus");
|
jest.spyOn(unlockButton as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((unlockButton as HTMLElement).focus).toBeCalled();
|
expect((unlockButton as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1173,7 +1196,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
|
||||||
jest.spyOn(newItemButton as HTMLElement, "focus");
|
jest.spyOn(newItemButton as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((newItemButton as HTMLElement).focus).toBeCalled();
|
expect((newItemButton as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1184,7 +1207,7 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
||||||
jest.spyOn(firstCipherItem as HTMLElement, "focus");
|
jest.spyOn(firstCipherItem as HTMLElement, "focus");
|
||||||
|
|
||||||
postWindowMessage({ command: "focusAutofillInlineMenuList" });
|
postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" });
|
||||||
|
|
||||||
expect((firstCipherItem as HTMLElement).focus).toBeCalled();
|
expect((firstCipherItem as HTMLElement).focus).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -1197,8 +1220,8 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.dispatchEvent(new Event("blur"));
|
globalThis.dispatchEvent(new Event("blur"));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "autofillInlineMenuBlurred", portKey },
|
{ command: "autofillInlineMenuBlurred", portKey, token: "test-token" },
|
||||||
"*",
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1220,8 +1243,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "previous", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "previous",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1229,8 +1257,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
|
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "next", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "next",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1238,8 +1271,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "redirectAutofillInlineMenuFocusOut", direction: "current", portKey },
|
{
|
||||||
"*",
|
command: "redirectAutofillInlineMenuFocusOut",
|
||||||
|
direction: "current",
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1274,8 +1312,13 @@ describe("AutofillInlineMenuList", () => {
|
|||||||
autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
|
autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
|
||||||
|
|
||||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||||
{ command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey },
|
{
|
||||||
"*",
|
command: "updateAutofillInlineMenuListHeight",
|
||||||
|
styles: { height: "300px" },
|
||||||
|
portKey,
|
||||||
|
token: "test-token",
|
||||||
|
},
|
||||||
|
expectedOrigin,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||||
import { generateRandomChars, setElementStyles } from "../../../../utils";
|
import { generateRandomChars, setElementStyles } from "../../../../utils";
|
||||||
import {
|
import {
|
||||||
InitAutofillInlineMenuElementMessage,
|
InitAutofillInlineMenuElementMessage,
|
||||||
@@ -73,7 +74,7 @@ export class AutofillInlineMenuContainer {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.token = generateRandomChars(32);
|
this.token = generateRandomChars(32);
|
||||||
this.extensionOrigin = chrome.runtime.getURL("").slice(0, -1);
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
globalThis.addEventListener("message", this.handleWindowMessage);
|
globalThis.addEventListener("message", this.handleWindowMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +204,9 @@ export class AutofillInlineMenuContainer {
|
|||||||
*/
|
*/
|
||||||
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
private handleWindowMessage = (event: MessageEvent<AutofillInlineMenuContainerWindowMessage>) => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
if (!message?.command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.isForeignWindowMessage(event)) {
|
if (this.isForeignWindowMessage(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -287,7 +291,10 @@ export class AutofillInlineMenuContainer {
|
|||||||
* every time the inline menu container is recreated.
|
* every time the inline menu container is recreated.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private isValidSessionToken(message: { token?: string }): boolean {
|
private isValidSessionToken(message: { token: string }): boolean {
|
||||||
|
if (!this.token || !message?.token || !message?.token.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return message.token === this.token;
|
return message.token === this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,8 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
styleSheetUrl: string,
|
styleSheetUrl: string,
|
||||||
translations: Record<string, string>,
|
translations: Record<string, string>,
|
||||||
portKey: string,
|
portKey: string,
|
||||||
token?: string,
|
|
||||||
): Promise<HTMLLinkElement> {
|
): Promise<HTMLLinkElement> {
|
||||||
this.portKey = portKey;
|
this.portKey = portKey;
|
||||||
if (token) {
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.translations = translations;
|
this.translations = translations;
|
||||||
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale"));
|
||||||
@@ -63,11 +59,16 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
* @param message - The message to post
|
* @param message - The message to post
|
||||||
*/
|
*/
|
||||||
protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) {
|
protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) {
|
||||||
const messageWithAuth: Record<string, unknown> = { portKey: this.portKey, ...message };
|
// never send messages containing authentication tokens without a valid token and an established messageOrigin
|
||||||
if (this.token) {
|
if (!this.token || !this.messageOrigin) {
|
||||||
messageWithAuth.token = this.token;
|
return;
|
||||||
}
|
}
|
||||||
globalThis.parent.postMessage(messageWithAuth, "*");
|
const messageWithAuth: Record<string, unknown> = {
|
||||||
|
portKey: this.portKey,
|
||||||
|
...message,
|
||||||
|
token: this.token,
|
||||||
|
};
|
||||||
|
globalThis.parent.postMessage(messageWithAuth, this.messageOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +106,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.source !== globalThis.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.messageOrigin) {
|
if (!this.messageOrigin) {
|
||||||
this.messageOrigin = event.origin;
|
this.messageOrigin = event.origin;
|
||||||
}
|
}
|
||||||
@@ -115,12 +120,23 @@ export class AutofillInlineMenuPageElement extends HTMLElement {
|
|||||||
|
|
||||||
const message = event?.data;
|
const message = event?.data;
|
||||||
|
|
||||||
if (
|
if (!message?.command) {
|
||||||
message?.token &&
|
return;
|
||||||
(message?.command === "initAutofillInlineMenuButton" ||
|
}
|
||||||
message?.command === "initAutofillInlineMenuList")
|
|
||||||
) {
|
const isInitCommand =
|
||||||
|
message.command === "initAutofillInlineMenuButton" ||
|
||||||
|
message.command === "initAutofillInlineMenuList";
|
||||||
|
|
||||||
|
if (isInitCommand) {
|
||||||
|
if (!message?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.token = message.token;
|
this.token = message.token;
|
||||||
|
} else {
|
||||||
|
if (!this.token || !message?.token || message.token !== this.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = this.windowMessageHandlers[message?.command];
|
const handler = this.windowMessageHandlers[message?.command];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates
|
|||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
id="bit-notification-bar-iframe"
|
id="bit-notification-bar-iframe"
|
||||||
src="chrome-extension://id/notification/bar.html"
|
src="chrome-extension://id/notification/bar.html?parentOrigin=http%3A%2F%2Flocalhost"
|
||||||
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
style="width: 100% !important; height: 100% !important; border: 0px !important; display: block !important; position: relative !important; transition: transform 0.15s ease-out, opacity 0.15s ease !important; border-radius: 4px !important; color-scheme: auto !important; transform: translateX(0) !important; opacity: 0;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -155,8 +155,9 @@ describe("OverlayNotificationsContentService", () => {
|
|||||||
{
|
{
|
||||||
command: "initNotificationBar",
|
command: "initNotificationBar",
|
||||||
initData: expect.any(Object),
|
initData: expect.any(Object),
|
||||||
|
parentOrigin: expect.any(String),
|
||||||
},
|
},
|
||||||
"*",
|
overlayNotificationsContentService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -257,7 +258,7 @@ describe("OverlayNotificationsContentService", () => {
|
|||||||
|
|
||||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||||
{ command: "saveCipherAttemptCompleted", error: undefined },
|
{ command: "saveCipherAttemptCompleted", error: undefined },
|
||||||
"*",
|
overlayNotificationsContentService["extensionOrigin"],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||||
import {
|
import {
|
||||||
NotificationBarIframeInitData,
|
NotificationBarIframeInitData,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@@ -22,6 +23,7 @@ export class OverlayNotificationsContentService
|
|||||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||||
private currentNotificationBarType: NotificationType | null = null;
|
private currentNotificationBarType: NotificationType | null = null;
|
||||||
|
private readonly extensionOrigin: string;
|
||||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||||
height: "400px",
|
height: "400px",
|
||||||
width: "430px",
|
width: "430px",
|
||||||
@@ -61,6 +63,7 @@ export class OverlayNotificationsContentService
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1);
|
||||||
void sendExtensionMessage("checkNotificationQueue");
|
void sendExtensionMessage("checkNotificationQueue");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +184,10 @@ export class OverlayNotificationsContentService
|
|||||||
this.currentNotificationBarType = initData.type;
|
this.currentNotificationBarType = initData.type;
|
||||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||||
this.notificationBarIframeElement.src = chrome.runtime.getURL("notification/bar.html");
|
const parentOrigin = globalThis.location.origin;
|
||||||
|
const iframeUrl = new URL(BrowserApi.getRuntimeURL("notification/bar.html"));
|
||||||
|
iframeUrl.searchParams.set("parentOrigin", parentOrigin);
|
||||||
|
this.notificationBarIframeElement.src = iframeUrl.toString();
|
||||||
setElementStyles(
|
setElementStyles(
|
||||||
this.notificationBarIframeElement,
|
this.notificationBarIframeElement,
|
||||||
{
|
{
|
||||||
@@ -254,7 +260,11 @@ export class OverlayNotificationsContentService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendMessageToNotificationBarIframe({ command: "initNotificationBar", initData });
|
this.sendMessageToNotificationBarIframe({
|
||||||
|
command: "initNotificationBar",
|
||||||
|
initData,
|
||||||
|
parentOrigin: globalThis.location.origin,
|
||||||
|
});
|
||||||
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
globalThis.removeEventListener("message", handleInitNotificationBarMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,7 +313,7 @@ export class OverlayNotificationsContentService
|
|||||||
*/
|
*/
|
||||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||||
if (this.notificationBarIframeElement) {
|
if (this.notificationBarIframeElement) {
|
||||||
this.notificationBarIframeElement.contentWindow.postMessage(message, "*");
|
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ export interface DomQueryService {
|
|||||||
mutationObserver?: MutationObserver,
|
mutationObserver?: MutationObserver,
|
||||||
forceDeepQueryAttempt?: boolean,
|
forceDeepQueryAttempt?: boolean,
|
||||||
): T[];
|
): T[];
|
||||||
checkPageContainsShadowDom(): boolean;
|
checkPageContainsShadowDom(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ describe("CollectAutofillContentService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the noFieldsFound property to true if the page has no forms or fields", async function () {
|
it("sets the noFieldsFond property to true if the page has no forms or fields", async function () {
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
collectAutofillContentService["noFieldsFound"] = false;
|
collectAutofillContentService["noFieldsFound"] = false;
|
||||||
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData");
|
||||||
@@ -2649,33 +2649,4 @@ describe("CollectAutofillContentService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("processMutations", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.runOnlyPendingTimers();
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("will require an update to page details if shadow DOM is present", () => {
|
|
||||||
jest
|
|
||||||
.spyOn(domQueryService as any, "checkPageContainsShadowDom")
|
|
||||||
.mockImplementationOnce(() => true);
|
|
||||||
|
|
||||||
collectAutofillContentService["requirePageDetailsUpdate"] = jest.fn();
|
|
||||||
|
|
||||||
collectAutofillContentService["mutationsQueue"] = [[], []];
|
|
||||||
|
|
||||||
collectAutofillContentService["processMutations"]();
|
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(domQueryService.checkPageContainsShadowDom).toHaveBeenCalled();
|
|
||||||
expect(collectAutofillContentService["mutationsQueue"]).toHaveLength(0);
|
|
||||||
expect(collectAutofillContentService["requirePageDetailsUpdate"]).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -997,13 +997,6 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
* within an idle callback to help with performance and prevent excessive updates.
|
* within an idle callback to help with performance and prevent excessive updates.
|
||||||
*/
|
*/
|
||||||
private processMutations = () => {
|
private processMutations = () => {
|
||||||
// If the page contains shadow DOM, we require a page details update from the autofill service.
|
|
||||||
// Will wait for an idle moment on main thread to execute, unless timeout has passed.
|
|
||||||
requestIdleCallbackPolyfill(
|
|
||||||
() => this.domQueryService.checkPageContainsShadowDom() && this.requirePageDetailsUpdate(),
|
|
||||||
{ timeout: 500 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const queueLength = this.mutationsQueue.length;
|
const queueLength = this.mutationsQueue.length;
|
||||||
|
|
||||||
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
|
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
|
||||||
@@ -1026,13 +1019,13 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
* Triggers several flags that indicate that a collection of page details should
|
* Triggers several flags that indicate that a collection of page details should
|
||||||
* occur again on a subsequent call after a mutation has been observed in the DOM.
|
* occur again on a subsequent call after a mutation has been observed in the DOM.
|
||||||
*/
|
*/
|
||||||
private requirePageDetailsUpdate = () => {
|
private flagPageDetailsUpdateIsRequired() {
|
||||||
this.domRecentlyMutated = true;
|
this.domRecentlyMutated = true;
|
||||||
if (this.autofillOverlayContentService) {
|
if (this.autofillOverlayContentService) {
|
||||||
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
|
||||||
}
|
}
|
||||||
this.noFieldsFound = false;
|
this.noFieldsFound = false;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes all mutation records encountered by the mutation observer.
|
* Processes all mutation records encountered by the mutation observer.
|
||||||
@@ -1060,7 +1053,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
|||||||
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
|
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
|
||||||
this.isAutofillElementNodeMutated(mutation.addedNodes))
|
this.isAutofillElementNodeMutated(mutation.addedNodes))
|
||||||
) {
|
) {
|
||||||
this.requirePageDetailsUpdate();
|
this.flagPageDetailsUpdateIsRequired();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ describe("DomQueryService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("queries form field elements that are nested within multiple ShadowDOM elements", () => {
|
it("queries form field elements that are nested within multiple ShadowDOM elements", () => {
|
||||||
|
domQueryService["pageContainsShadowDom"] = true;
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
||||||
const root2 = document.createElement("div");
|
const root2 = document.createElement("div");
|
||||||
@@ -94,6 +95,7 @@ describe("DomQueryService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("will fallback to using the TreeWalker API if a depth larger than 4 ShadowDOM elements is encountered", () => {
|
it("will fallback to using the TreeWalker API if a depth larger than 4 ShadowDOM elements is encountered", () => {
|
||||||
|
domQueryService["pageContainsShadowDom"] = true;
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
const shadowRoot1 = root.attachShadow({ mode: "open" });
|
||||||
const root2 = document.createElement("div");
|
const root2 = document.createElement("div");
|
||||||
|
|||||||
@@ -78,9 +78,8 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
/**
|
/**
|
||||||
* Checks if the page contains any shadow DOM elements.
|
* Checks if the page contains any shadow DOM elements.
|
||||||
*/
|
*/
|
||||||
checkPageContainsShadowDom = (): boolean => {
|
checkPageContainsShadowDom = (): void => {
|
||||||
this.pageContainsShadowDom = this.queryShadowRoots(globalThis.document.body, true).length > 0;
|
this.pageContainsShadowDom = this.queryShadowRoots(globalThis.document.body, true).length > 0;
|
||||||
return this.pageContainsShadowDom;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +108,7 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
): T[] {
|
): T[] {
|
||||||
let elements = this.queryElements<T>(root, queryString);
|
let elements = this.queryElements<T>(root, queryString);
|
||||||
|
|
||||||
const shadowRoots = this.pageContainsShadowDom ? this.recursivelyQueryShadowRoots(root) : [];
|
const shadowRoots = this.recursivelyQueryShadowRoots(root);
|
||||||
for (let index = 0; index < shadowRoots.length; index++) {
|
for (let index = 0; index < shadowRoots.length; index++) {
|
||||||
const shadowRoot = shadowRoots[index];
|
const shadowRoot = shadowRoots[index];
|
||||||
elements = elements.concat(this.queryElements<T>(shadowRoot, queryString));
|
elements = elements.concat(this.queryElements<T>(shadowRoot, queryString));
|
||||||
@@ -152,6 +151,10 @@ export class DomQueryService implements DomQueryServiceInterface {
|
|||||||
root: Document | ShadowRoot | Element,
|
root: Document | ShadowRoot | Element,
|
||||||
depth: number = 0,
|
depth: number = 0,
|
||||||
): ShadowRoot[] {
|
): ShadowRoot[] {
|
||||||
|
if (!this.pageContainsShadowDom) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (depth >= MAX_DEEP_QUERY_RECURSION_DEPTH) {
|
if (depth >= MAX_DEEP_QUERY_RECURSION_DEPTH) {
|
||||||
throw new Error("Max recursion depth reached");
|
throw new Error("Max recursion depth reached");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export function createInitAutofillInlineMenuButtonMessageMock(
|
|||||||
styleSheetUrl: "https://jest-testing-website.com",
|
styleSheetUrl: "https://jest-testing-website.com",
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
|
token: "test-token",
|
||||||
...customFields,
|
...customFields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -212,6 +213,7 @@ export function createInitAutofillInlineMenuListMessageMock(
|
|||||||
theme: ThemeTypes.Light,
|
theme: ThemeTypes.Light,
|
||||||
authStatus: AuthenticationStatus.Unlocked,
|
authStatus: AuthenticationStatus.Unlocked,
|
||||||
portKey: "portKey",
|
portKey: "portKey",
|
||||||
|
token: "test-token",
|
||||||
inlineMenuFillType: CipherType.Login,
|
inlineMenuFillType: CipherType.Login,
|
||||||
ciphers: [
|
ciphers: [
|
||||||
createAutofillOverlayCipherDataMock(1, {
|
createAutofillOverlayCipherDataMock(1, {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
|
||||||
export function triggerTestFailure() {
|
export function triggerTestFailure() {
|
||||||
expect(true).toBe("Test has failed.");
|
expect(true).toBe("Test has failed.");
|
||||||
}
|
}
|
||||||
@@ -11,7 +13,11 @@ export function flushPromises() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postWindowMessage(data: any, origin = "https://localhost/", source = window) {
|
export function postWindowMessage(
|
||||||
|
data: any,
|
||||||
|
origin: string = BrowserApi.getRuntimeURL("")?.slice(0, -1),
|
||||||
|
source: Window | MessageEventSource | null = window,
|
||||||
|
) {
|
||||||
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
|
globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -728,7 +728,9 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
|
||||||
|
this.singleUserStateProvider,
|
||||||
|
);
|
||||||
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||||
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
|
this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService);
|
||||||
|
|
||||||
@@ -859,8 +861,6 @@ export default class MainBackground {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
this.deviceTrustService = new DeviceTrustService(
|
this.deviceTrustService = new DeviceTrustService(
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
@@ -876,6 +876,7 @@ export default class MainBackground {
|
|||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.devicesService = new DevicesServiceImplementation(
|
this.devicesService = new DevicesServiceImplementation(
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ import "../platform/popup/locales";
|
|||||||
TabsV2Component,
|
TabsV2Component,
|
||||||
RemovePasswordComponent,
|
RemovePasswordComponent,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [CalloutModule],
|
||||||
providers: [CurrencyPipe, DatePipe],
|
providers: [CurrencyPipe, DatePipe],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
LoginEmailService,
|
LoginEmailService,
|
||||||
SsoUrlService,
|
SsoUrlService,
|
||||||
LogoutService,
|
LogoutService,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -607,7 +608,12 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: Fido2UserVerificationService,
|
provide: Fido2UserVerificationService,
|
||||||
useClass: Fido2UserVerificationService,
|
useClass: Fido2UserVerificationService,
|
||||||
deps: [PasswordRepromptService, UserVerificationService, DialogService],
|
deps: [
|
||||||
|
PasswordRepromptService,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
DialogService,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AnimationControlService,
|
provide: AnimationControlService,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
class="tw-text-sm tw-font-medium tw-cursor-pointer"
|
class="tw-text-sm tw-font-medium tw-cursor-pointer"
|
||||||
(click)="toggleSavedUrlExpandedState()"
|
(click)="toggleSavedUrlExpandedState()"
|
||||||
>
|
>
|
||||||
{{ (savedUrlsExpanded() ? "viewLess" : "viewAll") | i18n }}
|
{{ (savedUrlsExpanded() ? "showLess" : "showAll") | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-pt-2" [ngClass]="savedUrlsListClass()">
|
<div class="tw-pt-2" [ngClass]="savedUrlsListClass()">
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ describe("AutofillConfirmationDialogComponent", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const findShowAll = (inFx?: ComponentFixture<AutofillConfirmationDialogComponent>) =>
|
||||||
|
(inFx || fixture).nativeElement.querySelector(
|
||||||
|
"button.tw-text-sm.tw-font-medium.tw-cursor-pointer",
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
|
||||||
it("normalizes currentUrl and savedUrls via Utils.getHostname", () => {
|
it("normalizes currentUrl and savedUrls via Utils.getHostname", () => {
|
||||||
expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0));
|
expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0));
|
||||||
expect(component.currentUrl()).toBe("example.com");
|
expect(component.currentUrl()).toBe("example.com");
|
||||||
@@ -191,21 +196,47 @@ describe("AutofillConfirmationDialogComponent", () => {
|
|||||||
expect(text).toContain("two.example.com");
|
expect(text).toContain("two.example.com");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the 'view all' button when savedUrls > 1 and toggles the button text when clicked", () => {
|
it("shows the 'show all' button when savedUrls > 1", () => {
|
||||||
const findViewAll = () =>
|
const btn = findShowAll();
|
||||||
fixture.nativeElement.querySelector(
|
|
||||||
"button.tw-text-sm.tw-font-medium.tw-cursor-pointer",
|
|
||||||
) as HTMLButtonElement | null;
|
|
||||||
|
|
||||||
let btn = findViewAll();
|
|
||||||
expect(btn).toBeTruthy();
|
expect(btn).toBeTruthy();
|
||||||
|
expect(btn!.textContent).toContain("showAll");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the "show all" button when savedUrls is empty', async () => {
|
||||||
|
const newParams: AutofillConfirmationDialogParams = {
|
||||||
|
currentUrl: "https://bitwarden.com/help",
|
||||||
|
savedUrls: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { fixture: vf } = await createFreshFixture({ params: newParams });
|
||||||
|
vf.detectChanges();
|
||||||
|
const btn = findShowAll(vf);
|
||||||
|
expect(btn).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles toggling of the 'show all' button correctly", async () => {
|
||||||
|
const { fixture: vf, component: vc } = await createFreshFixture();
|
||||||
|
|
||||||
|
let btn = findShowAll(vf);
|
||||||
|
expect(btn).toBeTruthy();
|
||||||
|
expect(vc.savedUrlsExpanded()).toBe(false);
|
||||||
|
expect(btn!.textContent).toContain("showAll");
|
||||||
|
|
||||||
|
// click to expand
|
||||||
btn!.click();
|
btn!.click();
|
||||||
fixture.detectChanges();
|
vf.detectChanges();
|
||||||
|
|
||||||
btn = findViewAll();
|
btn = findShowAll(vf);
|
||||||
expect(btn!.textContent).toContain("viewLess");
|
expect(btn!.textContent).toContain("showLess");
|
||||||
expect(component.savedUrlsExpanded()).toBe(true);
|
expect(vc.savedUrlsExpanded()).toBe(true);
|
||||||
|
|
||||||
|
// click to collapse
|
||||||
|
btn!.click();
|
||||||
|
vf.detectChanges();
|
||||||
|
|
||||||
|
btn = findShowAll(vf);
|
||||||
|
expect(btn!.textContent).toContain("showAll");
|
||||||
|
expect(vc.savedUrlsExpanded()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows autofillWithoutAdding text on autofill button when viewOnly is false", () => {
|
it("shows autofillWithoutAdding text on autofill button when viewOnly is false", () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
|
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
|
||||||
import { CopyableCipherFields } from "@bitwarden/sdk-internal";
|
import { CopyableCipherFields } from "@bitwarden/sdk-internal";
|
||||||
import { CopyAction, CopyCipherFieldDirective } from "@bitwarden/vault";
|
import { CopyFieldAction, CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
|
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ type CipherItem = {
|
|||||||
/** Translation key for the respective value */
|
/** Translation key for the respective value */
|
||||||
key: string;
|
key: string;
|
||||||
/** Property key on `CipherView` to retrieve the copy value */
|
/** Property key on `CipherView` to retrieve the copy value */
|
||||||
field: CopyAction;
|
field: CopyFieldAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
@@ -48,7 +48,7 @@ export class ItemCopyActionsComponent {
|
|||||||
* singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
|
* singleCopyableLogin uses appCopyField instead of appCopyClick. This allows for the TOTP
|
||||||
* code to be copied correctly. See #14167
|
* code to be copied correctly. See #14167
|
||||||
*/
|
*/
|
||||||
get singleCopyableLogin() {
|
get singleCopyableLogin(): CipherItem | null {
|
||||||
const loginItems: CipherItem[] = [
|
const loginItems: CipherItem[] = [
|
||||||
{ key: "copyUsername", field: "username" },
|
{ key: "copyUsername", field: "username" },
|
||||||
{ key: "copyPassword", field: "password" },
|
{ key: "copyPassword", field: "password" },
|
||||||
@@ -62,7 +62,7 @@ export class ItemCopyActionsComponent {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
key: this.i18nService.t("copyUsername"),
|
key: this.i18nService.t("copyUsername"),
|
||||||
field: "username",
|
field: "username" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return this.findSingleCopyableItem(loginItems);
|
return this.findSingleCopyableItem(loginItems);
|
||||||
|
|||||||
@@ -51,10 +51,26 @@
|
|||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@if (showArchive$ | async) {
|
||||||
@if (canArchive$ | async) {
|
@if (canArchive$ | async) {
|
||||||
<button type="button" bitMenuItem (click)="archive()">
|
<button type="button" bitMenuItem (click)="archive()">
|
||||||
{{ "archiveVerb" | i18n }}
|
{{ "archiveVerb" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="badge.promptForPremium($event)"
|
||||||
|
[attr.aria-label]="'upgradeToUseArchive' | i18n"
|
||||||
|
>
|
||||||
|
<div class="tw-flex tw-flex-nowrap tw-items-center tw-gap-2">
|
||||||
|
{{ "archiveVerb" | i18n }}
|
||||||
|
<div aria-hidden>
|
||||||
|
<app-premium-badge #badge></app-premium-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if (canDelete$ | async) {
|
@if (canDelete$ | async) {
|
||||||
<button type="button" bitMenuItem (click)="delete()">
|
<button type="button" bitMenuItem (click)="delete()">
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ describe("ItemMoreOptionsComponent", () => {
|
|||||||
},
|
},
|
||||||
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
||||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
||||||
{ provide: CipherArchiveService, useValue: { userCanArchive$: () => of(true) } },
|
{
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: () => of(true) },
|
||||||
|
},
|
||||||
{ provide: ToastService, useValue: { showToast: () => {} } },
|
{ provide: ToastService, useValue: { showToast: () => {} } },
|
||||||
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
||||||
{ provide: PasswordRepromptService, useValue: passwordRepromptService },
|
{ provide: PasswordRepromptService, useValue: passwordRepromptService },
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||||
import { filter } from "rxjs/operators";
|
import { filter } from "rxjs/operators";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
|
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -17,6 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||||
@@ -46,7 +49,18 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-item-more-options",
|
selector: "app-item-more-options",
|
||||||
templateUrl: "./item-more-options.component.html",
|
templateUrl: "./item-more-options.component.html",
|
||||||
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
imports: [
|
||||||
|
ItemModule,
|
||||||
|
IconButtonModule,
|
||||||
|
MenuModule,
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
RouterModule,
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ItemMoreOptionsComponent {
|
export class ItemMoreOptionsComponent {
|
||||||
private _cipher$ = new BehaviorSubject<CipherViewLike>({} as CipherViewLike);
|
private _cipher$ = new BehaviorSubject<CipherViewLike>({} as CipherViewLike);
|
||||||
@@ -127,18 +141,11 @@ export class ItemMoreOptionsComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Observable Boolean checking if item can show Archive menu option */
|
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
|
||||||
protected canArchive$ = combineLatest([
|
|
||||||
this._cipher$,
|
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||||
this.accountService.activeAccount$.pipe(
|
|
||||||
getUserId,
|
getUserId,
|
||||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||||
),
|
|
||||||
]).pipe(
|
|
||||||
filter(([cipher, userId]) => cipher != null && userId != null),
|
|
||||||
map(([cipher, canArchive]) => {
|
|
||||||
return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
protected canDelete$ = this._cipher$.pipe(
|
protected canDelete$ = this._cipher$.pipe(
|
||||||
@@ -377,6 +384,11 @@ export class ItemMoreOptionsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async archive() {
|
async archive() {
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher);
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "archiveItem" },
|
title: { key: "archiveItem" },
|
||||||
content: { key: "archiveItemConfirmDesc" },
|
content: { key: "archiveItemConfirmDesc" },
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||||
</div>
|
</div>
|
||||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
@if (cipher.hasAttachments) {
|
@if (CipherViewLikeUtils.hasAttachments(cipher)) {
|
||||||
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
|
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
|
||||||
}
|
}
|
||||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
<span slot="secondary">{{ CipherViewLikeUtils.subtitle(cipher) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<bit-item-action slot="end">
|
<bit-item-action slot="end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import {
|
||||||
|
CipherViewLike,
|
||||||
|
CipherViewLikeUtils,
|
||||||
|
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
@@ -71,12 +74,14 @@ export class ArchiveComponent {
|
|||||||
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
|
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||||
|
|
||||||
protected loading$ = this.archivedCiphers$.pipe(
|
protected loading$ = this.archivedCiphers$.pipe(
|
||||||
map(() => false),
|
map(() => false),
|
||||||
startWith(true),
|
startWith(true),
|
||||||
);
|
);
|
||||||
|
|
||||||
async view(cipher: CipherView) {
|
async view(cipher: CipherViewLike) {
|
||||||
if (!(await this.canInteract(cipher))) {
|
if (!(await this.canInteract(cipher))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,7 +91,7 @@ export class ArchiveComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(cipher: CipherView) {
|
async edit(cipher: CipherViewLike) {
|
||||||
if (!(await this.canInteract(cipher))) {
|
if (!(await this.canInteract(cipher))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,7 +101,7 @@ export class ArchiveComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(cipher: CipherView) {
|
async delete(cipher: CipherViewLike) {
|
||||||
if (!(await this.canInteract(cipher))) {
|
if (!(await this.canInteract(cipher))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -113,7 +118,7 @@ export class ArchiveComponent {
|
|||||||
const activeUserId = await firstValueFrom(this.userId$);
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
|
await this.cipherService.softDeleteWithServer(cipher.id as string, activeUserId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
return;
|
return;
|
||||||
@@ -125,13 +130,16 @@ export class ArchiveComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async unarchive(cipher: CipherView) {
|
async unarchive(cipher: CipherViewLike) {
|
||||||
if (!(await this.canInteract(cipher))) {
|
if (!(await this.canInteract(cipher))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeUserId = await firstValueFrom(this.userId$);
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
|
||||||
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
|
await this.cipherArchiveService.unarchiveWithServer(
|
||||||
|
cipher.id as unknown as CipherId,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@@ -139,12 +147,12 @@ export class ArchiveComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clone(cipher: CipherView) {
|
async clone(cipher: CipherViewLike) {
|
||||||
if (!(await this.canInteract(cipher))) {
|
if (!(await this.canInteract(cipher))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher.login?.hasFido2Credentials) {
|
if (CipherViewLikeUtils.hasFido2Credentials(cipher)) {
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "passkeyNotCopied" },
|
title: { key: "passkeyNotCopied" },
|
||||||
content: { key: "passkeyNotCopiedAlert" },
|
content: { key: "passkeyNotCopiedAlert" },
|
||||||
@@ -171,8 +179,8 @@ export class ArchiveComponent {
|
|||||||
* @param cipher
|
* @param cipher
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private canInteract(cipher: CipherView) {
|
private canInteract(cipher: CipherViewLike) {
|
||||||
if (cipher.decryptionFailure) {
|
if (CipherViewLikeUtils.decryptionFailure(cipher)) {
|
||||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||||
cipherIds: [cipher.id as CipherId],
|
cipherIds: [cipher.id as CipherId],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||||
></i>
|
></i>
|
||||||
<i
|
<i
|
||||||
*ngIf="cipher.hasAttachments"
|
*ngIf="hasAttachments(cipher)"
|
||||||
class="bwi bwi-paperclip bwi-sm"
|
class="bwi bwi-paperclip bwi-sm"
|
||||||
[appA11yTitle]="'attachments' | i18n"
|
[appA11yTitle]="'attachments' | i18n"
|
||||||
></i>
|
></i>
|
||||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
<span slot="secondary">{{ getSubtitle(cipher) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<ng-container slot="end" *ngIf="cipher.permissions.restore">
|
<ng-container slot="end" *ngIf="cipher.permissions.restore">
|
||||||
<bit-item-action>
|
<bit-item-action>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="restore(cipher)"
|
(click)="restore(cipher)"
|
||||||
*ngIf="!cipher.decryptionFailure"
|
*ngIf="!hasDecryptionFailure(cipher)"
|
||||||
>
|
>
|
||||||
{{ "restore" | i18n }}
|
{{ "restore" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { CipherId } from "@bitwarden/common/types/guid";
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
@@ -85,10 +84,40 @@ export class TrashListItemsContainerComponent {
|
|||||||
return collections[0]?.name;
|
return collections[0]?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(cipher: CipherView) {
|
/**
|
||||||
|
* Check if a cipher has attachments. CipherView has a hasAttachments getter,
|
||||||
|
* while CipherListView has an attachments count property.
|
||||||
|
*/
|
||||||
|
hasAttachments(cipher: PopupCipherViewLike): boolean {
|
||||||
|
if ("hasAttachments" in cipher) {
|
||||||
|
return cipher.hasAttachments;
|
||||||
|
}
|
||||||
|
return cipher.attachments > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the subtitle for a cipher. CipherView has a subTitle getter,
|
||||||
|
* while CipherListView has a subtitle property.
|
||||||
|
*/
|
||||||
|
getSubtitle(cipher: PopupCipherViewLike): string | undefined {
|
||||||
|
if ("subTitle" in cipher) {
|
||||||
|
return cipher.subTitle;
|
||||||
|
}
|
||||||
|
return cipher.subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a cipher has a decryption failure. CipherView has this property,
|
||||||
|
* while CipherListView does not.
|
||||||
|
*/
|
||||||
|
hasDecryptionFailure(cipher: PopupCipherViewLike): boolean {
|
||||||
|
return "decryptionFailure" in cipher && cipher.decryptionFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(cipher: PopupCipherViewLike) {
|
||||||
try {
|
try {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
await this.cipherService.restoreWithServer(cipher.id, activeUserId);
|
await this.cipherService.restoreWithServer(cipher.id as string, activeUserId);
|
||||||
|
|
||||||
await this.router.navigate(["/trash"]);
|
await this.router.navigate(["/trash"]);
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -101,7 +130,7 @@ export class TrashListItemsContainerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(cipher: CipherView) {
|
async delete(cipher: PopupCipherViewLike) {
|
||||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
|
||||||
if (!repromptPassed) {
|
if (!repromptPassed) {
|
||||||
@@ -120,7 +149,7 @@ export class TrashListItemsContainerComponent {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
await this.cipherService.deleteWithServer(cipher.id, activeUserId);
|
await this.cipherService.deleteWithServer(cipher.id as string, activeUserId);
|
||||||
|
|
||||||
await this.router.navigate(["/trash"]);
|
await this.router.navigate(["/trash"]);
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -133,8 +162,9 @@ export class TrashListItemsContainerComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onViewCipher(cipher: CipherView) {
|
async onViewCipher(cipher: PopupCipherViewLike) {
|
||||||
if (cipher.decryptionFailure) {
|
// CipherListView doesn't have decryptionFailure, so we use optional chaining
|
||||||
|
if ("decryptionFailure" in cipher && cipher.decryptionFailure) {
|
||||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||||
cipherIds: [cipher.id as CipherId],
|
cipherIds: [cipher.id as CipherId],
|
||||||
});
|
});
|
||||||
@@ -147,7 +177,7 @@ export class TrashListItemsContainerComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.router.navigate(["/view-cipher"], {
|
await this.router.navigate(["/view-cipher"], {
|
||||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
queryParams: { cipherId: cipher.id as string, type: cipher.type },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,27 @@
|
|||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
@if (userCanArchive() || showArchiveFilter()) {
|
@if (showArchiveItem()) {
|
||||||
|
@if (userCanArchive()) {
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/archive">
|
<a bit-item-content routerLink="/archive">
|
||||||
{{ "archiveNoun" | i18n }}
|
{{ "archiveNoun" | i18n }}
|
||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
} @else {
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content [routerLink]="userHasArchivedItems() ? '/archive' : '/premium'">
|
||||||
|
<span class="tw-flex tw-items-center tw-gap-2">
|
||||||
|
{{ "archiveNoun" | i18n }}
|
||||||
|
@if (!userHasArchivedItems()) {
|
||||||
|
<app-premium-badge></app-premium-badge>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/trash">
|
<a bit-item-content routerLink="/trash">
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { toSignal } from "@angular/core/rxjs-interop";
|
import { toSignal } from "@angular/core/rxjs-interop";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { firstValueFrom, switchMap } from "rxjs";
|
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
|
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
|||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
import { BrowserPremiumUpgradePromptService } from "../services/browser-premium-upgrade-prompt.service";
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||||
@@ -32,20 +35,28 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
BadgeComponent,
|
BadgeComponent,
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||||
lastSync = "--";
|
lastSync = "--";
|
||||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
// Check if user is premium user, they will be able to archive items
|
|
||||||
protected readonly userCanArchive = toSignal(
|
protected readonly userCanArchive = toSignal(
|
||||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if user has archived items (does not check if user is premium)
|
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||||
protected readonly showArchiveFilter = toSignal(
|
|
||||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
protected readonly userHasArchivedItems = toSignal(
|
||||||
|
this.userId$.pipe(
|
||||||
|
switchMap((userId) =>
|
||||||
|
this.cipherArchiveService.archivedCiphers$(userId).pipe(map((c) => c.length > 0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { newGuid } from "@bitwarden/guid";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||||
@@ -31,21 +35,24 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
let fido2UserVerificationService: Fido2UserVerificationService;
|
let fido2UserVerificationService: Fido2UserVerificationService;
|
||||||
|
|
||||||
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
||||||
let userVerificationService: MockProxy<UserVerificationService>;
|
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
let dialogService: MockProxy<DialogService>;
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
let cipher: CipherView;
|
let cipher: CipherView;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
passwordRepromptService = mock<PasswordRepromptService>();
|
passwordRepromptService = mock<PasswordRepromptService>();
|
||||||
userVerificationService = mock<UserVerificationService>();
|
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
dialogService = mock<DialogService>();
|
dialogService = mock<DialogService>();
|
||||||
|
accountService = mockAccountServiceWith(newGuid() as UserId);
|
||||||
|
|
||||||
cipher = createCipherView();
|
cipher = createCipherView();
|
||||||
|
|
||||||
fido2UserVerificationService = new Fido2UserVerificationService(
|
fido2UserVerificationService = new Fido2UserVerificationService(
|
||||||
passwordRepromptService,
|
passwordRepromptService,
|
||||||
userVerificationService,
|
userDecryptionOptionsService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
||||||
@@ -67,7 +74,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
@@ -82,7 +89,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
true,
|
true,
|
||||||
@@ -98,7 +105,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
true,
|
true,
|
||||||
@@ -114,7 +121,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
@@ -176,7 +183,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
@@ -191,7 +198,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
false,
|
false,
|
||||||
@@ -207,7 +214,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
false,
|
false,
|
||||||
@@ -223,7 +230,7 @@ describe("Fido2UserVerificationService", () => {
|
|||||||
|
|
||||||
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||||
cipher.reprompt = CipherRepromptType.Password;
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
const result = await fido2UserVerificationService.handleUserVerification(
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@@ -15,8 +16,9 @@ import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
|||||||
export class Fido2UserVerificationService {
|
export class Fido2UserVerificationService {
|
||||||
constructor(
|
constructor(
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +80,15 @@ export class Fido2UserVerificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleMasterPasswordReprompt(): Promise<boolean> {
|
private async handleMasterPasswordReprompt(): Promise<boolean> {
|
||||||
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
|
||||||
|
if (!activeAccount?.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMasterPassword = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(activeAccount.id),
|
||||||
|
);
|
||||||
|
|
||||||
// TDE users have no master password, so we need to use the UserVerification prompt
|
// TDE users have no master password, so we need to use the UserVerification prompt
|
||||||
return hasMasterPassword
|
return hasMasterPassword
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base",
|
"extends": "../../tsconfig.base",
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
"../../libs/common/src/autofill/constants",
|
"../../libs/common/src/autofill/constants",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"tldts": "7.0.1",
|
"tldts": "7.0.18",
|
||||||
"zxcvbn": "4.4.2"
|
"zxcvbn": "4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -512,7 +512,9 @@ export class ServiceContainer {
|
|||||||
")";
|
")";
|
||||||
|
|
||||||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(
|
||||||
|
this.singleUserStateProvider,
|
||||||
|
);
|
||||||
this.ssoUrlService = new SsoUrlService();
|
this.ssoUrlService = new SsoUrlService();
|
||||||
|
|
||||||
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
this.organizationService = new DefaultOrganizationService(this.stateProvider);
|
||||||
@@ -702,6 +704,7 @@ export class ServiceContainer {
|
|||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.configService,
|
this.configService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.loginStrategyService = new LoginStrategyService(
|
this.loginStrategyService = new LoginStrategyService(
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
|
|||||||
@@ -123,7 +123,9 @@ describe("WebSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||||
import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
import { firstValueFrom, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||||
@@ -42,8 +41,7 @@ export class AccountComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private configService: ConfigService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ export class AccountComponent implements OnInit, OnDestroy {
|
|||||||
map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)),
|
map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword());
|
const hasMasterPassword$ = this.userDecryptionOptionsService.hasMasterPasswordById$(userId);
|
||||||
|
|
||||||
this.showChangeEmail$ = hasMasterPassword$;
|
this.showChangeEmail$ = hasMasterPassword$;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
|
import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password";
|
||||||
import { InputPasswordFlow } from "@bitwarden/auth/angular";
|
import { InputPasswordFlow } from "@bitwarden/auth/angular";
|
||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { CalloutModule } from "@bitwarden/components";
|
import { CalloutModule } from "@bitwarden/components";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -24,12 +26,15 @@ export class PasswordSettingsComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const userHasMasterPassword = await firstValueFrom(
|
const userHasMasterPassword = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.hasMasterPassword$,
|
this.userDecryptionOptionsService.hasMasterPasswordById$(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userHasMasterPassword) {
|
if (!userHasMasterPassword) {
|
||||||
await this.router.navigate(["/settings/security/two-factor"]);
|
await this.router.navigate(["/settings/security/two-factor"]);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module";
|
import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module";
|
||||||
@@ -23,20 +22,28 @@ export class SecurityKeysComponent implements OnInit {
|
|||||||
showChangeKdf = true;
|
showChangeKdf = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showChangeKdf = await this.userVerificationService.hasMasterPassword();
|
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
|
this.showChangeKdf = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(userId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async viewUserApiKey() {
|
async viewUserApiKey() {
|
||||||
const entityId = await firstValueFrom(
|
const entityId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
throw new Error("Active account not found");
|
||||||
|
}
|
||||||
|
|
||||||
await ApiKeyComponent.open(this.dialogService, {
|
await ApiKeyComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
keyType: "user",
|
keyType: "user",
|
||||||
@@ -55,6 +62,11 @@ export class SecurityKeysComponent implements OnInit {
|
|||||||
const entityId = await firstValueFrom(
|
const entityId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!entityId) {
|
||||||
|
throw new Error("Active account not found");
|
||||||
|
}
|
||||||
|
|
||||||
await ApiKeyComponent.open(this.dialogService, {
|
await ApiKeyComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
keyType: "user",
|
keyType: "user",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { Observable } from "rxjs";
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
@@ -20,7 +22,8 @@ export class SecurityComponent implements OnInit {
|
|||||||
consolidatedSessionTimeoutComponent$: Observable<boolean>;
|
consolidatedSessionTimeoutComponent$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userVerificationService: UserVerificationService,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
|
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
|
||||||
@@ -29,6 +32,9 @@ export class SecurityComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
|
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
|
this.showChangePassword = userId
|
||||||
|
? await firstValueFrom(this.userDecryptionOptionsService.hasMasterPasswordById$(userId))
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,10 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
|||||||
combineLatest([
|
combineLatest([
|
||||||
this.organization$,
|
this.organization$,
|
||||||
resetPasswordPolicies$,
|
resetPasswordPolicies$,
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) => this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)),
|
||||||
|
),
|
||||||
managingOrg$,
|
managingOrg$,
|
||||||
])
|
])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
|||||||
@@ -198,10 +198,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
|||||||
userId: UserId,
|
userId: UserId,
|
||||||
) {
|
) {
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
);
|
);
|
||||||
userDecryptionOpts.hasMasterPassword = true;
|
userDecryptionOpts.hasMasterPassword = true;
|
||||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||||
|
userId,
|
||||||
|
userDecryptionOpts,
|
||||||
|
);
|
||||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject,
|
||||||
|
);
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
credentials.newServerMasterKeyHash,
|
credentials.newServerMasterKeyHash,
|
||||||
@@ -362,7 +364,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
userDecryptionOptions,
|
userDecryptionOptions,
|
||||||
);
|
);
|
||||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
@@ -560,7 +563,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
userDecryptionOptions,
|
userDecryptionOptions,
|
||||||
);
|
);
|
||||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
useClass: UserDecryptionOptionsService,
|
useClass: UserDecryptionOptionsService,
|
||||||
deps: [StateProvider],
|
deps: [SingleUserStateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: UserDecryptionOptionsServiceAbstraction,
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
@@ -1292,6 +1292,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const userDecryptionOptions = await firstValueFrom(
|
const userDecryptionOptions = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(this.activeAccountId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ export class SsoComponent implements OnInit {
|
|||||||
|
|
||||||
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tdeEnabled = userDecryptionOpts.trustedDeviceOption
|
const tdeEnabled = userDecryptionOpts.trustedDeviceOption
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ describe("TwoFactorAuthComponent", () => {
|
|||||||
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
|
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
|
||||||
mockUserDecryptionOpts.withMasterPassword,
|
mockUserDecryptionOpts.withMasterPassword,
|
||||||
);
|
);
|
||||||
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
mockUserDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
selectedUserDecryptionOptions,
|
||||||
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [TestTwoFactorComponent],
|
declarations: [TestTwoFactorComponent],
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userDecryptionOpts = await firstValueFrom(
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
||||||
|
|||||||
@@ -1,34 +1,45 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { UserDecryptionOptions } from "../models";
|
import { UserDecryptionOptions } from "../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public service for reading user decryption options.
|
||||||
|
* For use in components and services that need to evaluate user decryption settings.
|
||||||
|
*/
|
||||||
export abstract class UserDecryptionOptionsServiceAbstraction {
|
export abstract class UserDecryptionOptionsServiceAbstraction {
|
||||||
/**
|
/**
|
||||||
* Returns what decryption options are available for the current user.
|
* Returns the user decryption options for the given user id.
|
||||||
* @remark This is sent from the server on authentication.
|
* Will only emit when options are set (does not emit null/undefined
|
||||||
|
* for an unpopulated state), and should not be called in an unauthenticated context.
|
||||||
|
* @param userId The user id to check.
|
||||||
*/
|
*/
|
||||||
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
|
abstract userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions>;
|
||||||
/**
|
/**
|
||||||
* Uses user decryption options to determine if current user has a master password.
|
* Uses user decryption options to determine if current user has a master password.
|
||||||
* @remark This is sent from the server, and does not indicate if the master password
|
* @remark This is sent from the server, and does not indicate if the master password
|
||||||
* was used to login and/or if a master key is saved locally.
|
* was used to login and/or if a master key is saved locally.
|
||||||
*/
|
*/
|
||||||
abstract hasMasterPassword$: Observable<boolean>;
|
abstract hasMasterPasswordById$(userId: UserId): Observable<boolean>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user decryption options for the given user id.
|
|
||||||
* @param userId The user id to check.
|
|
||||||
*/
|
|
||||||
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal service for managing user decryption options.
|
||||||
|
* For use only in authentication flows that need to update decryption options
|
||||||
|
* (e.g., login strategies). Extends consumer methods from {@link UserDecryptionOptionsServiceAbstraction}.
|
||||||
|
* @remarks Most consumers should use UserDecryptionOptionsServiceAbstraction instead.
|
||||||
|
*/
|
||||||
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
|
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
|
||||||
/**
|
/**
|
||||||
* Sets the current decryption options for the user, contains the current configuration
|
* Sets the current decryption options for the user. Contains the current configuration
|
||||||
* of the users account related to how they can decrypt their vault.
|
* of the users account related to how they can decrypt their vault.
|
||||||
* @remark Intended to be used when user decryption options are received from server, does
|
* @remark Intended to be used when user decryption options are received from server, does
|
||||||
* not update the server. Consider syncing instead of updating locally.
|
* not update the server. Consider syncing instead of updating locally.
|
||||||
* @param userDecryptionOptions Current user decryption options received from server.
|
* @param userDecryptionOptions Current user decryption options received from server.
|
||||||
*/
|
*/
|
||||||
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
|
abstract setUserDecryptionOptionsById(
|
||||||
|
userId: UserId,
|
||||||
|
userDecryptionOptions: UserDecryptionOptions,
|
||||||
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ describe("LoginStrategy", () => {
|
|||||||
|
|
||||||
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
expect(environmentService.seedUserEnvironment).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
UserDecryptionOptions.fromResponse(idTokenResponse),
|
UserDecryptionOptions.fromResponse(idTokenResponse),
|
||||||
);
|
);
|
||||||
expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ export abstract class LoginStrategy {
|
|||||||
|
|
||||||
// We must set user decryption options before retrieving vault timeout settings
|
// We must set user decryption options before retrieving vault timeout settings
|
||||||
// as the user decryption options help determine the available timeout actions.
|
// as the user decryption options help determine the available timeout actions.
|
||||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(
|
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||||
|
userId,
|
||||||
UserDecryptionOptions.fromResponse(tokenResponse),
|
UserDecryptionOptions.fromResponse(tokenResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ describe("SsoLoginStrategy", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const userDecryptionOptions = new UserDecryptionOptions();
|
const userDecryptionOptions = new UserDecryptionOptions();
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions);
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
|
of(userDecryptionOptions),
|
||||||
|
);
|
||||||
|
|
||||||
ssoLoginStrategy = new SsoLoginStrategy(
|
ssoLoginStrategy = new SsoLoginStrategy(
|
||||||
{} as SsoLoginStrategyData,
|
{} as SsoLoginStrategyData,
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
// Check for TDE-related conditions
|
// Check for TDE-related conditions
|
||||||
const userDecryptionOptions = await firstValueFrom(
|
const userDecryptionOptions = await firstValueFrom(
|
||||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userDecryptionOptions) {
|
if (!userDecryptionOptions) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { FakeSingleUserStateProvider } from "@bitwarden/common/spec";
|
||||||
import {
|
|
||||||
FakeAccountService,
|
|
||||||
FakeStateProvider,
|
|
||||||
mockAccountServiceWith,
|
|
||||||
} from "@bitwarden/common/spec";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { newGuid } from "@bitwarden/guid";
|
||||||
|
|
||||||
import { UserDecryptionOptions } from "../../models/domain/user-decryption-options";
|
import { UserDecryptionOptions } from "../../models/domain/user-decryption-options";
|
||||||
|
|
||||||
@@ -17,15 +13,10 @@ import {
|
|||||||
|
|
||||||
describe("UserDecryptionOptionsService", () => {
|
describe("UserDecryptionOptionsService", () => {
|
||||||
let sut: UserDecryptionOptionsService;
|
let sut: UserDecryptionOptionsService;
|
||||||
|
let fakeStateProvider: FakeSingleUserStateProvider;
|
||||||
const fakeUserId = Utils.newGuid() as UserId;
|
|
||||||
let fakeAccountService: FakeAccountService;
|
|
||||||
let fakeStateProvider: FakeStateProvider;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeAccountService = mockAccountServiceWith(fakeUserId);
|
fakeStateProvider = new FakeSingleUserStateProvider();
|
||||||
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
|
||||||
|
|
||||||
sut = new UserDecryptionOptionsService(fakeStateProvider);
|
sut = new UserDecryptionOptionsService(fakeStateProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,55 +33,71 @@ describe("UserDecryptionOptionsService", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("userDecryptionOptions$", () => {
|
describe("userDecryptionOptionsById$", () => {
|
||||||
it("should return the active user's decryption options", async () => {
|
it("should return user decryption options for a specific user", async () => {
|
||||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
const userId = newGuid() as UserId;
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.userDecryptionOptions$);
|
fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(sut.userDecryptionOptionsById$(userId));
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
expect(result).toEqual(userDecryptionOptions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hasMasterPassword$", () => {
|
describe("hasMasterPasswordById$", () => {
|
||||||
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
|
it("should return true when user has a master password", async () => {
|
||||||
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
|
const userId = newGuid() as UserId;
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.hasMasterPassword$);
|
fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(sut.hasMasterPasswordById$(userId));
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return false when user does not have a master password", async () => {
|
||||||
|
const userId = newGuid() as UserId;
|
||||||
|
const optionsWithoutMasterPassword = {
|
||||||
|
...userDecryptionOptions,
|
||||||
|
hasMasterPassword: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
fakeStateProvider
|
||||||
|
.getFake(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.nextState(optionsWithoutMasterPassword);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(sut.hasMasterPasswordById$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("userDecryptionOptionsById$", () => {
|
describe("setUserDecryptionOptionsById", () => {
|
||||||
it("should return the user decryption options for the given user", async () => {
|
it("should set user decryption options for a specific user", async () => {
|
||||||
const givenUser = Utils.newGuid() as UserId;
|
const userId = newGuid() as UserId;
|
||||||
await fakeAccountService.addAccount(givenUser, {
|
|
||||||
name: "Test User 1",
|
|
||||||
email: "test1@email.com",
|
|
||||||
emailVerified: false,
|
|
||||||
});
|
|
||||||
await fakeStateProvider.setUserState(
|
|
||||||
USER_DECRYPTION_OPTIONS,
|
|
||||||
userDecryptionOptions,
|
|
||||||
givenUser,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
|
await sut.setUserDecryptionOptionsById(userId, userDecryptionOptions);
|
||||||
|
|
||||||
|
const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS);
|
||||||
|
const result = await firstValueFrom(fakeState.state$);
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
expect(result).toEqual(userDecryptionOptions);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("setUserDecryptionOptions", () => {
|
it("should overwrite existing user decryption options", async () => {
|
||||||
it("should set the active user's decryption options", async () => {
|
const userId = newGuid() as UserId;
|
||||||
await sut.setUserDecryptionOptions(userDecryptionOptions);
|
const initialOptions = { ...userDecryptionOptions, hasMasterPassword: false };
|
||||||
|
const updatedOptions = { ...userDecryptionOptions, hasMasterPassword: true };
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS);
|
||||||
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
|
fakeState.nextState(initialOptions);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(userDecryptionOptions);
|
await sut.setUserDecryptionOptionsById(userId, updatedOptions);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(fakeState.state$);
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedOptions);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { Observable, filter, map } from "rxjs";
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Observable, map } from "rxjs";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActiveUserState,
|
SingleUserStateProvider,
|
||||||
StateProvider,
|
|
||||||
USER_DECRYPTION_OPTIONS_DISK,
|
USER_DECRYPTION_OPTIONS_DISK,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
// FIXME: remove `src` and fix import
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import { UserId } from "@bitwarden/common/src/types/guid";
|
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||||
import { UserDecryptionOptions } from "../../models";
|
import { UserDecryptionOptions } from "../../models";
|
||||||
@@ -27,25 +22,26 @@ export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptio
|
|||||||
export class UserDecryptionOptionsService
|
export class UserDecryptionOptionsService
|
||||||
implements InternalUserDecryptionOptionsServiceAbstraction
|
implements InternalUserDecryptionOptionsServiceAbstraction
|
||||||
{
|
{
|
||||||
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
|
constructor(private singleUserStateProvider: SingleUserStateProvider) {}
|
||||||
|
|
||||||
userDecryptionOptions$: Observable<UserDecryptionOptions>;
|
userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions> {
|
||||||
hasMasterPassword$: Observable<boolean>;
|
return this.singleUserStateProvider
|
||||||
|
.get(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.state$.pipe(filter((options): options is UserDecryptionOptions => options != null));
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
hasMasterPasswordById$(userId: UserId): Observable<boolean> {
|
||||||
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
|
return this.userDecryptionOptionsById$(userId).pipe(
|
||||||
|
map((options) => options.hasMasterPassword ?? false),
|
||||||
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
|
|
||||||
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
|
|
||||||
map((options) => options?.hasMasterPassword ?? false),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions> {
|
async setUserDecryptionOptionsById(
|
||||||
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
|
userId: UserId,
|
||||||
}
|
userDecryptionOptions: UserDecryptionOptions,
|
||||||
|
): Promise<void> {
|
||||||
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
|
await this.singleUserStateProvider
|
||||||
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
|
.get(userId, USER_DECRYPTION_OPTIONS)
|
||||||
|
.update((_) => userDecryptionOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export abstract class UserVerificationService {
|
|||||||
* @param userId The user id to check. If not provided, the current user is used
|
* @param userId The user id to check. If not provided, the current user is used
|
||||||
* @returns True if the user has a master password
|
* @returns True if the user has a master password
|
||||||
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
|
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
|
||||||
|
* @remark To facilitate deprecation, many call sites were removed as part of PM-26413.
|
||||||
|
* Those remaining are blocked by currently-disallowed imports of auth/common.
|
||||||
|
* PM-27009 has been filed to track completion of this deprecation.
|
||||||
*/
|
*/
|
||||||
abstract hasMasterPassword(userId?: string): Promise<boolean>;
|
abstract hasMasterPassword(userId?: string): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { of } from "rxjs";
|
|||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import {
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
UserDecryptionOptions,
|
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
|
||||||
} from "@bitwarden/auth/common";
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import {
|
import {
|
||||||
@@ -146,11 +143,7 @@ describe("UserVerificationService", () => {
|
|||||||
|
|
||||||
describe("server verification type", () => {
|
describe("server verification type", () => {
|
||||||
it("correctly returns master password availability", async () => {
|
it("correctly returns master password availability", async () => {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true));
|
||||||
of({
|
|
||||||
hasMasterPassword: true,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await sut.getAvailableVerificationOptions("server");
|
const result = await sut.getAvailableVerificationOptions("server");
|
||||||
|
|
||||||
@@ -168,11 +161,7 @@ describe("UserVerificationService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("correctly returns OTP availability", async () => {
|
it("correctly returns OTP availability", async () => {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false));
|
||||||
of({
|
|
||||||
hasMasterPassword: false,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await sut.getAvailableVerificationOptions("server");
|
const result = await sut.getAvailableVerificationOptions("server");
|
||||||
|
|
||||||
@@ -526,11 +515,7 @@ describe("UserVerificationService", () => {
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function setMasterPasswordAvailability(hasMasterPassword: boolean) {
|
function setMasterPasswordAvailability(hasMasterPassword: boolean) {
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(hasMasterPassword));
|
||||||
of({
|
|
||||||
hasMasterPassword: hasMasterPassword,
|
|
||||||
} as UserDecryptionOptions),
|
|
||||||
);
|
|
||||||
masterPasswordService.masterKeyHash$.mockReturnValue(
|
masterPasswordService.masterKeyHash$.mockReturnValue(
|
||||||
of(hasMasterPassword ? "masterKeyHash" : null),
|
of(hasMasterPassword ? "masterKeyHash" : null),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -258,16 +258,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||||||
}
|
}
|
||||||
|
|
||||||
async hasMasterPassword(userId?: string): Promise<boolean> {
|
async hasMasterPassword(userId?: string): Promise<boolean> {
|
||||||
if (userId) {
|
const resolvedUserId = userId ?? (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
const decryptionOptions = await firstValueFrom(
|
|
||||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (decryptionOptions?.hasMasterPassword != undefined) {
|
if (!resolvedUserId) {
|
||||||
return decryptionOptions.hasMasterPassword;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
// Ideally, this method would accept a UserId over string. To avoid scope creep in PM-26413, we are
|
||||||
|
// doing the cast here. Future work should be done to make this type-safe, and should be considered
|
||||||
|
// as part of PM-27009.
|
||||||
|
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId as UserId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export enum FeatureFlag {
|
|||||||
/* Admin Console Team */
|
/* Admin Console Team */
|
||||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||||
AutoConfirm = "pm-19934-auto-confirm-organization-users",
|
AutoConfirm = "pm-19934-auto-confirm-organization-users",
|
||||||
BlockClaimedDomainAccountCreation = "block-claimed-domain-account-creation",
|
BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration",
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
|
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { firstValueFrom, map, Observable, Subject } from "rxjs";
|
import { firstValueFrom, map, Observable, Subject, switchMap } from "rxjs";
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@@ -9,6 +9,7 @@ import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||||
@@ -87,12 +88,20 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
|||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {
|
) {
|
||||||
this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe(
|
this.supportsDeviceTrust$ = this.accountService.activeAccount$.pipe(
|
||||||
|
switchMap((account) => {
|
||||||
|
if (account == null) {
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
return this.userDecryptionOptionsService.userDecryptionOptionsById$(account.id).pipe(
|
||||||
map((options) => {
|
map((options) => {
|
||||||
return options?.trustedDeviceOption != null;
|
return options?.trustedDeviceOption != null;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean> {
|
supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean> {
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ describe("deviceTrustService", () => {
|
|||||||
platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage);
|
platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage);
|
||||||
|
|
||||||
decryptionOptions.next({} as any);
|
decryptionOptions.next({} as any);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(decryptionOptions);
|
||||||
|
|
||||||
return new DeviceTrustService(
|
return new DeviceTrustService(
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
@@ -930,6 +930,7 @@ describe("deviceTrustService", () => {
|
|||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
logService,
|
logService,
|
||||||
configService,
|
configService,
|
||||||
|
accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
|||||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterKeyWrappedUserKey">;
|
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterKeyWrappedUserKey">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data required to unlock with the master password.
|
* Encapsulates the data needed to unlock a vault using a master password.
|
||||||
|
* It contains the masterKeyWrappedUserKey along with the KDF settings and salt used to derive the master key.
|
||||||
|
* It is currently backwards compatible to master-key based unlock, but this will not be the case in the future.
|
||||||
|
* Features relating to master-password-based unlock should use this abstraction.
|
||||||
*/
|
*/
|
||||||
export class MasterPasswordUnlockData {
|
export class MasterPasswordUnlockData {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -66,7 +69,9 @@ export class MasterPasswordUnlockData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data required to authenticate with the master password.
|
* Encapsulates the data required to authenticate using a master password.
|
||||||
|
* It contains the masterPasswordAuthenticationHash, along with the KDF settings and salt used to derive it.
|
||||||
|
* The encapsulated abstraction prevents authentication issues resulting from unsynchronized state.
|
||||||
*/
|
*/
|
||||||
export type MasterPasswordAuthenticationData = {
|
export type MasterPasswordAuthenticationData = {
|
||||||
salt: MasterPasswordSalt;
|
salt: MasterPasswordSalt;
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
policyService = mock<PolicyService>();
|
policyService = mock<PolicyService>();
|
||||||
|
|
||||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
|
userDecryptionOptionsSubject,
|
||||||
map((options) => options?.hasMasterPassword ?? false),
|
);
|
||||||
|
userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(
|
||||||
|
userDecryptionOptionsSubject.pipe(map((options) => options?.hasMasterPassword ?? false)),
|
||||||
);
|
);
|
||||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||||
userDecryptionOptionsSubject,
|
userDecryptionOptionsSubject,
|
||||||
@@ -127,6 +129,23 @@ describe("VaultTimeoutSettingsService", () => {
|
|||||||
|
|
||||||
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return only LogOut when userId is not provided and there is no active account", async () => {
|
||||||
|
// Set up accountService to return null for activeAccount
|
||||||
|
accountService.activeAccount$ = of(null);
|
||||||
|
pinStateService.isPinSet.mockResolvedValue(false);
|
||||||
|
biometricStateService.biometricUnlockEnabled$ = of(false);
|
||||||
|
|
||||||
|
// Call availableVaultTimeoutActions$ which internally calls userHasMasterPassword without a userId
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since there's no active account, userHasMasterPassword returns false,
|
||||||
|
// meaning no master password is available, so Lock should not be available
|
||||||
|
expect(result).toEqual([VaultTimeoutAction.LogOut]);
|
||||||
|
expect(result).not.toContain(VaultTimeoutAction.Lock);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("canLock", () => {
|
describe("canLock", () => {
|
||||||
|
|||||||
@@ -290,14 +290,19 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async userHasMasterPassword(userId: string): Promise<boolean> {
|
private async userHasMasterPassword(userId: string): Promise<boolean> {
|
||||||
|
let resolvedUserId: UserId;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const decryptionOptions = await firstValueFrom(
|
resolvedUserId = userId as UserId;
|
||||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
|
||||||
);
|
|
||||||
|
|
||||||
return !!decryptionOptions?.hasMasterPassword;
|
|
||||||
} else {
|
} else {
|
||||||
return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
}
|
if (!activeAccount) {
|
||||||
|
return false; // No account, can't have master password
|
||||||
|
}
|
||||||
|
resolvedUserId = activeAccount.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,7 +414,10 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
creationDate: this.creationDate.toISOString(),
|
creationDate: this.creationDate.toISOString(),
|
||||||
deletedDate: this.deletedDate?.toISOString(),
|
deletedDate: this.deletedDate?.toISOString(),
|
||||||
archivedDate: this.archivedDate?.toISOString(),
|
archivedDate: this.archivedDate?.toISOString(),
|
||||||
reprompt: this.reprompt,
|
reprompt:
|
||||||
|
this.reprompt === CipherRepromptType.Password
|
||||||
|
? CipherRepromptType.Password
|
||||||
|
: CipherRepromptType.None,
|
||||||
// Initialize all cipher-type-specific properties as undefined
|
// Initialize all cipher-type-specific properties as undefined
|
||||||
login: undefined,
|
login: undefined,
|
||||||
identity: undefined,
|
identity: undefined,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
|||||||
"tw-relative",
|
"tw-relative",
|
||||||
"tw-transition",
|
"tw-transition",
|
||||||
"tw-cursor-pointer",
|
"tw-cursor-pointer",
|
||||||
|
"disabled:tw-cursor-default",
|
||||||
"tw-inline-block",
|
"tw-inline-block",
|
||||||
"tw-align-sub",
|
"tw-align-sub",
|
||||||
"tw-flex-none", // Flexbox fix for bit-form-control
|
"tw-flex-none", // Flexbox fix for bit-form-control
|
||||||
@@ -62,7 +63,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
|||||||
"[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-offset-2",
|
"[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-offset-2",
|
||||||
"[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-primary-600",
|
"[&:not(bit-form-control_*)]:focus-visible:before:tw-ring-primary-600",
|
||||||
|
|
||||||
"disabled:before:tw-cursor-auto",
|
"disabled:before:tw-cursor-default",
|
||||||
"disabled:before:tw-border",
|
"disabled:before:tw-border",
|
||||||
"disabled:before:hover:tw-border",
|
"disabled:before:hover:tw-border",
|
||||||
"disabled:before:tw-bg-secondary-100",
|
"disabled:before:tw-bg-secondary-100",
|
||||||
|
|||||||
35
libs/components/src/header/header.component.html
Normal file
35
libs/components/src/header/header.component.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<header
|
||||||
|
class="-tw-mt-6 -tw-mx-8 tw-mb-3 tw-flex tw-flex-col tw-py-6 tw-px-8 has-[[data-tabs]:not(:empty)]:tw-border-0 has-[[data-tabs]:not(:empty)]:tw-border-b has-[[data-tabs]:not(:empty)]:tw-border-solid has-[[data-tabs]:not(:empty)]:tw-border-secondary-100 has-[[data-tabs]:not(:empty)]:tw-bg-background-alt has-[[data-tabs]:not(:empty)]:tw-pb-0"
|
||||||
|
>
|
||||||
|
<div class="tw-flex">
|
||||||
|
<div class="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col tw-gap-2">
|
||||||
|
<ng-content select="[slot=breadcrumbs]"></ng-content>
|
||||||
|
<h1
|
||||||
|
bitTypography="h1"
|
||||||
|
noMargin
|
||||||
|
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
|
||||||
|
[title]="title()"
|
||||||
|
>
|
||||||
|
<div class="tw-truncate">
|
||||||
|
@if (icon()) {
|
||||||
|
<i class="bwi {{ icon() }}" aria-hidden="true"></i>
|
||||||
|
}
|
||||||
|
|
||||||
|
{{ title() }}
|
||||||
|
</div>
|
||||||
|
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="tw-ml-auto tw-flex tw-flex-col tw-gap-4">
|
||||||
|
<div class="tw-flex tw-min-w-max tw-items-center tw-justify-end tw-gap-2">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<div class="tw-ml-auto empty:tw-hidden">
|
||||||
|
<ng-content select="[slot=secondary]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-tabs class="-tw-mx-4 -tw-mb-px empty:tw-hidden">
|
||||||
|
<ng-content select="[slot=tabs]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
19
libs/components/src/header/header.component.ts
Normal file
19
libs/components/src/header/header.component.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-header",
|
||||||
|
templateUrl: "./header.component.html",
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class HeaderComponent {
|
||||||
|
/**
|
||||||
|
* The title of the page
|
||||||
|
*/
|
||||||
|
readonly title = input.required<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon to show before the title
|
||||||
|
*/
|
||||||
|
readonly icon = input<string>();
|
||||||
|
}
|
||||||
189
libs/components/src/header/header.stories.ts
Normal file
189
libs/components/src/header/header.stories.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { importProvidersFrom } from "@angular/core";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
import {
|
||||||
|
applicationConfig,
|
||||||
|
componentWrapperDecorator,
|
||||||
|
Meta,
|
||||||
|
moduleMetadata,
|
||||||
|
StoryObj,
|
||||||
|
} from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import {
|
||||||
|
AvatarModule,
|
||||||
|
BreadcrumbsModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
IconModule,
|
||||||
|
InputModule,
|
||||||
|
MenuModule,
|
||||||
|
NavigationModule,
|
||||||
|
TabsModule,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { I18nMockService } from "../utils";
|
||||||
|
|
||||||
|
import { HeaderComponent } from "./header.component";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Header",
|
||||||
|
component: HeaderComponent,
|
||||||
|
decorators: [
|
||||||
|
componentWrapperDecorator(
|
||||||
|
(story) => `<div class="tw-min-h-screen tw-flex-1 tw-p-6 tw-text-main">${story}</div>`,
|
||||||
|
),
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
AvatarModule,
|
||||||
|
BreadcrumbsModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
IconModule,
|
||||||
|
InputModule,
|
||||||
|
MenuModule,
|
||||||
|
NavigationModule,
|
||||||
|
TabsModule,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
applicationConfig({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useFactory: () => {
|
||||||
|
return new I18nMockService({
|
||||||
|
moreBreadcrumbs: "More breadcrumbs",
|
||||||
|
loading: "Loading",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
importProvidersFrom(
|
||||||
|
RouterModule.forRoot(
|
||||||
|
[
|
||||||
|
{ path: "", redirectTo: "foo", pathMatch: "full" },
|
||||||
|
{ path: "foo", component: HeaderComponent },
|
||||||
|
{ path: "bar", component: HeaderComponent },
|
||||||
|
],
|
||||||
|
{ useHash: true },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<HeaderComponent>;
|
||||||
|
|
||||||
|
export const KitchenSink: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||||
|
<bit-breadcrumbs slot="breadcrumbs">
|
||||||
|
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||||
|
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||||
|
</bit-breadcrumbs>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
placeholder="Ask Jeeves"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<button type="button" bitIconButton="bwi-filter" label="Switch products"></button>
|
||||||
|
<bit-avatar text="Will"></bit-avatar>
|
||||||
|
<button bitButton buttonType="primary">New</button>
|
||||||
|
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||||
|
<bit-tab-nav-bar slot="tabs">
|
||||||
|
<bit-tab-link [route]="['foo']">Foo</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['bar']">Bar</bit-tab-link>
|
||||||
|
</bit-tab-nav-bar>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Basic: Story = {
|
||||||
|
render: (args: any) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" />
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithLongTitle: Story = {
|
||||||
|
render: (arg: any) => ({
|
||||||
|
props: arg,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||||
|
<ng-container slot="title-suffix"><i class="bwi bwi-key"></i></ng-container>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithBreadcrumbs: Story = {
|
||||||
|
render: (args: any) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||||
|
<bit-breadcrumbs slot="breadcrumbs">
|
||||||
|
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||||
|
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||||
|
</bit-breadcrumbs>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithSearch: Story = {
|
||||||
|
render: (args: any) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
placeholder="Ask Jeeves"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithSecondaryContent: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||||
|
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithTabs: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||||
|
<bit-tab-nav-bar slot="tabs">
|
||||||
|
<bit-tab-link [route]="['foo']">Foo</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['bar']">Bar</bit-tab-link>
|
||||||
|
</bit-tab-nav-bar>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithTitleSuffixComponent: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||||
|
<ng-container slot="title-suffix"><i class="bwi bwi-spinner bwi-spin"></i></ng-container>
|
||||||
|
</bit-header>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
1
libs/components/src/header/index.ts
Normal file
1
libs/components/src/header/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./header.component";
|
||||||
@@ -19,6 +19,7 @@ export * from "./dialog";
|
|||||||
export * from "./disclosure";
|
export * from "./disclosure";
|
||||||
export * from "./drawer";
|
export * from "./drawer";
|
||||||
export * from "./form-field";
|
export * from "./form-field";
|
||||||
|
export * from "./header";
|
||||||
export * from "./icon-button";
|
export * from "./icon-button";
|
||||||
export * from "./icon";
|
export * from "./icon";
|
||||||
export * from "./icon-tile";
|
export * from "./icon-tile";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core";
|
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
|
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only shows the element if the user can delete the cipher.
|
* Only shows the element if the user can delete the cipher.
|
||||||
@@ -15,7 +15,7 @@ export class CanDeleteCipherDirective implements OnDestroy {
|
|||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||||
@Input("appCanDeleteCipher") set cipher(cipher: CipherView) {
|
@Input("appCanDeleteCipher") set cipher(cipher: CipherViewLike) {
|
||||||
this.viewContainer.clear();
|
this.viewContainer.clear();
|
||||||
|
|
||||||
this.cipherAuthorizationService
|
this.cipherAuthorizationService
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class CopyCipherFieldDirective implements OnChanges {
|
|||||||
alias: "appCopyField",
|
alias: "appCopyField",
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
action!: Exclude<CopyAction, "hiddenField">;
|
action!: CopyAction;
|
||||||
|
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ export {
|
|||||||
AtRiskPasswordCalloutData,
|
AtRiskPasswordCalloutData,
|
||||||
} from "./services/at-risk-password-callout.service";
|
} from "./services/at-risk-password-callout.service";
|
||||||
export { PasswordRepromptService } from "./services/password-reprompt.service";
|
export { PasswordRepromptService } from "./services/password-reprompt.service";
|
||||||
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
|
export {
|
||||||
|
CopyCipherFieldService,
|
||||||
|
CopyAction,
|
||||||
|
CopyFieldAction,
|
||||||
|
} from "./services/copy-cipher-field.service";
|
||||||
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
||||||
export { OrgIconDirective } from "./components/org-icon.directive";
|
export { OrgIconDirective } from "./components/org-icon.directive";
|
||||||
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
|
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ export type CopyAction =
|
|||||||
| "publicKey"
|
| "publicKey"
|
||||||
| "keyFingerprint";
|
| "keyFingerprint";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy actions that can be used with the appCopyField directive.
|
||||||
|
* Excludes "hiddenField" which requires special handling.
|
||||||
|
*/
|
||||||
|
export type CopyFieldAction = Exclude<CopyAction, "hiddenField">;
|
||||||
|
|
||||||
type CopyActionInfo = {
|
type CopyActionInfo = {
|
||||||
/**
|
/**
|
||||||
* The i18n key for the type of field being copied. Will be used to display a toast message.
|
* The i18n key for the type of field being copied. Will be used to display a toast message.
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -68,7 +68,7 @@
|
|||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"tabbable": "6.3.0",
|
"tabbable": "6.3.0",
|
||||||
"tldts": "7.0.1",
|
"tldts": "7.0.18",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"utf-8-validate": "6.0.5",
|
"utf-8-validate": "6.0.5",
|
||||||
"zone.js": "0.15.1",
|
"zone.js": "0.15.1",
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"tldts": "7.0.1",
|
"tldts": "7.0.18",
|
||||||
"zxcvbn": "4.4.2"
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -39263,21 +39263,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tldts": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz",
|
||||||
"integrity": "sha512-C3TdHZKykiDkxPIKUYCDWyYpcLQ8bDYvF/RGfH66UikQX3Kro7ij2/WGNYgp5EfxXB4+Tu5H728uAgYGNE1eaQ==",
|
"integrity": "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tldts-core": "^7.0.1"
|
"tldts-core": "^7.0.18"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tldts": "bin/cli.js"
|
"tldts": "bin/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tldts-core": {
|
"node_modules/tldts-core": {
|
||||||
"version": "7.0.9",
|
"version": "7.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
|
||||||
"integrity": "sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==",
|
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"tabbable": "6.3.0",
|
"tabbable": "6.3.0",
|
||||||
"tldts": "7.0.1",
|
"tldts": "7.0.18",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"utf-8-validate": "6.0.5",
|
"utf-8-validate": "6.0.5",
|
||||||
"zone.js": "0.15.1",
|
"zone.js": "0.15.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user