1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Merge branch 'main' into autofill/pm-8027-inline-menu-appears-within-input-fields-that-do-not-relate-to-user-login

This commit is contained in:
Cesar Gonzalez
2024-05-29 13:43:31 -05:00
committed by GitHub
60 changed files with 984 additions and 165 deletions

View File

@@ -218,6 +218,12 @@
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }] "no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }]
} }
}, },
{
"files": ["libs/tools/send/send-ui/src/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }]
}
},
{ {
"files": ["libs/vault/src/**/*.ts"], "files": ["libs/vault/src/**/*.ts"],
"rules": { "rules": {

View File

@@ -21,6 +21,7 @@
./libs/platform/README.md ./libs/platform/README.md
./libs/tools/README.md ./libs/tools/README.md
./libs/tools/export/vault-export/README.md ./libs/tools/export/vault-export/README.md
./libs/tools/send/README.md
./libs/vault/README.md ./libs/vault/README.md
./README.md ./README.md
./LICENSE_BITWARDEN.txt ./LICENSE_BITWARDEN.txt

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.5.0", "version": "2024.5.1",
"scripts": { "scripts": {
"build": "cross-env MANIFEST_VERSION=3 webpack", "build": "cross-env MANIFEST_VERSION=3 webpack",
"build:mv2": "webpack", "build:mv2": "webpack",

View File

@@ -42,6 +42,7 @@ type OverlayPortMessage = {
type FocusedFieldData = { type FocusedFieldData = {
focusedFieldStyles: Partial<CSSStyleDeclaration>; focusedFieldStyles: Partial<CSSStyleDeclaration>;
focusedFieldRects: Partial<DOMRect>; focusedFieldRects: Partial<DOMRect>;
tabId?: number;
}; };
type OverlayCipherData = { type OverlayCipherData = {
@@ -66,14 +67,14 @@ type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSende
type OverlayBackgroundExtensionMessageHandlers = { type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
openAutofillOverlay: () => void; openAutofillOverlay: () => void;
autofillOverlayElementClosed: ({ message }: BackgroundMessageParam) => void; autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
getAutofillOverlayVisibility: () => void; getAutofillOverlayVisibility: () => void;
checkAutofillOverlayFocused: () => void; checkAutofillOverlayFocused: () => void;
focusAutofillOverlayList: () => void; focusAutofillOverlayList: () => void;
updateAutofillOverlayPosition: ({ message }: BackgroundMessageParam) => void; updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void;
updateFocusedFieldData: ({ message }: BackgroundMessageParam) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void;
addEditCipherSubmitted: () => void; addEditCipherSubmitted: () => void;

View File

@@ -517,7 +517,7 @@ describe("OverlayBackground", () => {
expect(returnValue).toBe(undefined); expect(returnValue).toBe(undefined);
expect(sendResponse).not.toHaveBeenCalled(); expect(sendResponse).not.toHaveBeenCalled();
expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message); expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender);
}); });
it("will return a response if the message handler returns a response", async () => { it("will return a response if the message handler returns a response", async () => {
@@ -570,6 +570,26 @@ describe("OverlayBackground", () => {
await initOverlayElementPorts(); await initOverlayElementPorts();
}); });
it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => {
const port1 = mock<chrome.runtime.Port>();
const port2 = mock<chrome.runtime.Port>();
overlayBackground["expiredPorts"] = [port1, port2];
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendExtensionRuntimeMessage(
{
command: "autofillOverlayElementClosed",
overlayElement: AutofillOverlayElement.Button,
},
sender,
);
expect(port1.disconnect).toHaveBeenCalled();
expect(port2.disconnect).toHaveBeenCalled();
});
it("disconnects the button element port", () => { it("disconnects the button element port", () => {
sendExtensionRuntimeMessage({ sendExtensionRuntimeMessage({
command: "autofillOverlayElementClosed", command: "autofillOverlayElementClosed",
@@ -729,6 +749,23 @@ describe("OverlayBackground", () => {
}); });
}); });
it("skips updating the position if the most recently focused field is different than the message sender", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 });
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
sendExtensionRuntimeMessage({ command: "updateAutofillOverlayPosition" }, sender);
expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "updateIframePosition",
styles: expect.anything(),
});
expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({
command: "updateIframePosition",
styles: expect.anything(),
});
});
it("updates the overlay button's position", () => { it("updates the overlay button's position", () => {
const focusedFieldData = createFocusedFieldDataMock(); const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData }); sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
@@ -796,12 +833,14 @@ describe("OverlayBackground", () => {
}); });
it("will post a message to the overlay list facilitating an update of the list's position", () => { it("will post a message to the overlay list facilitating an update of the list's position", () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock(); const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData }); sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
overlayBackground["updateOverlayPosition"]({ overlayBackground["updateOverlayPosition"](
overlayElement: AutofillOverlayElement.List, { overlayElement: AutofillOverlayElement.List },
}); sender,
);
sendExtensionRuntimeMessage({ sendExtensionRuntimeMessage({
command: "updateAutofillOverlayPosition", command: "updateAutofillOverlayPosition",
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
@@ -1017,9 +1056,10 @@ describe("OverlayBackground", () => {
expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css");
expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled();
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith({ expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
overlayElement: AutofillOverlayElement.List, { overlayElement: AutofillOverlayElement.List },
}); listPortSpy.sender,
);
}); });
it("sets up the overlay button port if the port connection is for the overlay button", async () => { it("sets up the overlay button port if the port connection is for the overlay button", async () => {
@@ -1032,9 +1072,19 @@ describe("OverlayBackground", () => {
expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled();
expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css");
expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); expect(overlayBackground["getTranslations"]).toHaveBeenCalled();
expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith({ expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith(
overlayElement: AutofillOverlayElement.Button, { overlayElement: AutofillOverlayElement.Button },
}); buttonPortSpy.sender,
);
});
it("stores an existing overlay port so that it can be disconnected at a later time", async () => {
overlayBackground["overlayButtonPort"] = mock<chrome.runtime.Port>();
await initOverlayElementPorts({ initList: false, initButton: true });
await flushPromises();
expect(overlayBackground["expiredPorts"].length).toBe(1);
}); });
it("gets the system theme", async () => { it("gets the system theme", async () => {

View File

@@ -54,19 +54,22 @@ class OverlayBackground implements OverlayBackgroundInterface {
private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut;
private overlayButtonPort: chrome.runtime.Port; private overlayButtonPort: chrome.runtime.Port;
private overlayListPort: chrome.runtime.Port; private overlayListPort: chrome.runtime.Port;
private expiredPorts: chrome.runtime.Port[] = [];
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private overlayPageTranslations: Record<string, string>; private overlayPageTranslations: Record<string, string>;
private iconsServerUrl: string; private iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
openAutofillOverlay: () => this.openOverlay(false), openAutofillOverlay: () => this.openOverlay(false),
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message), autofillOverlayElementClosed: ({ message, sender }) =>
this.overlayElementClosed(message, sender),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
getAutofillOverlayVisibility: () => this.getOverlayVisibility(), getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
checkAutofillOverlayFocused: () => this.checkOverlayFocused(), checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
focusAutofillOverlayList: () => this.focusOverlayList(), focusAutofillOverlayList: () => this.focusOverlayList(),
updateAutofillOverlayPosition: ({ message }) => this.updateOverlayPosition(message), updateAutofillOverlayPosition: ({ message, sender }) =>
this.updateOverlayPosition(message, sender),
updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message),
updateFocusedFieldData: ({ message }) => this.setFocusedFieldData(message), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message), unlockCompleted: ({ message }) => this.unlockCompleted(message),
addEditCipherSubmitted: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(),
@@ -302,8 +305,18 @@ class OverlayBackground implements OverlayBackgroundInterface {
* the list and button ports and sets them to null. * the list and button ports and sets them to null.
* *
* @param overlayElement - The overlay element that was closed, either the list or button * @param overlayElement - The overlay element that was closed, either the list or button
* @param sender - The sender of the port message
*/ */
private overlayElementClosed({ overlayElement }: OverlayBackgroundExtensionMessage) { private overlayElementClosed(
{ overlayElement }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
if (sender.tab.id !== this.focusedFieldData?.tabId) {
this.expiredPorts.forEach((port) => port.disconnect());
this.expiredPorts = [];
return;
}
if (overlayElement === AutofillOverlayElement.Button) { if (overlayElement === AutofillOverlayElement.Button) {
this.overlayButtonPort?.disconnect(); this.overlayButtonPort?.disconnect();
this.overlayButtonPort = null; this.overlayButtonPort = null;
@@ -320,9 +333,13 @@ class OverlayBackground implements OverlayBackgroundInterface {
* is based on the focused field's position and dimensions. * is based on the focused field's position and dimensions.
* *
* @param overlayElement - The overlay element to update, either the list or button * @param overlayElement - The overlay element to update, either the list or button
* @param sender - The sender of the port message
*/ */
private updateOverlayPosition({ overlayElement }: { overlayElement?: string }) { private updateOverlayPosition(
if (!overlayElement) { { overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
return; return;
} }
@@ -396,9 +413,13 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sets the focused field data to the data passed in the extension message. * Sets the focused field data to the data passed in the extension message.
* *
* @param focusedFieldData - Contains the rects and styles of the focused field. * @param focusedFieldData - Contains the rects and styles of the focused field.
* @param sender - The sender of the extension message
*/ */
private setFocusedFieldData({ focusedFieldData }: OverlayBackgroundExtensionMessage) { private setFocusedFieldData(
this.focusedFieldData = focusedFieldData; { focusedFieldData }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id };
} }
/** /**
@@ -690,17 +711,11 @@ class OverlayBackground implements OverlayBackgroundInterface {
private handlePortOnConnect = async (port: chrome.runtime.Port) => { private handlePortOnConnect = async (port: chrome.runtime.Port) => {
const isOverlayListPort = port.name === AutofillOverlayPort.List; const isOverlayListPort = port.name === AutofillOverlayPort.List;
const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; const isOverlayButtonPort = port.name === AutofillOverlayPort.Button;
if (!isOverlayListPort && !isOverlayButtonPort) { if (!isOverlayListPort && !isOverlayButtonPort) {
return; return;
} }
if (isOverlayListPort) { this.storeOverlayPort(port);
this.overlayListPort = port;
} else {
this.overlayButtonPort = port;
}
port.onMessage.addListener(this.handleOverlayElementPortMessage); port.onMessage.addListener(this.handleOverlayElementPortMessage);
port.postMessage({ port.postMessage({
command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`,
@@ -710,13 +725,47 @@ class OverlayBackground implements OverlayBackgroundInterface {
translations: this.getTranslations(), translations: this.getTranslations(),
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
}); });
this.updateOverlayPosition({ this.updateOverlayPosition(
overlayElement: isOverlayListPort {
? AutofillOverlayElement.List overlayElement: isOverlayListPort
: AutofillOverlayElement.Button, ? AutofillOverlayElement.List
}); : AutofillOverlayElement.Button,
},
port.sender,
);
}; };
/**
* Stores the connected overlay port and sets up any existing ports to be disconnected.
*
* @param port - The port to store
| */
private storeOverlayPort(port: chrome.runtime.Port) {
if (port.name === AutofillOverlayPort.List) {
this.storeExpiredOverlayPort(this.overlayListPort);
this.overlayListPort = port;
return;
}
if (port.name === AutofillOverlayPort.Button) {
this.storeExpiredOverlayPort(this.overlayButtonPort);
this.overlayButtonPort = port;
}
}
/**
* When registering a new connection, we want to ensure that the port is disconnected.
* This method places an existing port in the expiredPorts array to be disconnected
* at a later time.
*
* @param port - The port to store in the expiredPorts array
*/
private storeExpiredOverlayPort(port: chrome.runtime.Port | null) {
if (port) {
this.expiredPorts.push(port);
}
}
/** /**
* Handles messages sent to the overlay list or button ports. * Handles messages sent to the overlay list or button ports.
* *

View File

@@ -186,9 +186,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.overlayButtonElement.remove(); this.overlayButtonElement.remove();
this.isOverlayButtonVisible = false; this.isOverlayButtonVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("autofillOverlayElementClosed", {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button, overlayElement: AutofillOverlayElement.Button,
}); });
this.removeOverlayRepositionEventListeners(); this.removeOverlayRepositionEventListeners();
@@ -204,9 +202,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.overlayListElement.remove(); this.overlayListElement.remove();
this.isOverlayListVisible = false; this.isOverlayListVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("autofillOverlayElementClosed", {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
}); });
} }

View File

@@ -249,6 +249,7 @@ function createFocusedFieldDataMock(customFields = {}) {
paddingRight: "6px", paddingRight: "6px",
paddingLeft: "6px", paddingLeft: "6px",
}, },
tabId: 1,
...customFields, ...customFields,
}; };
} }

View File

@@ -334,7 +334,7 @@ export default class MainBackground {
ssoLoginService: SsoLoginServiceAbstraction; ssoLoginService: SsoLoginServiceAbstraction;
billingAccountProfileStateService: BillingAccountProfileStateService; billingAccountProfileStateService: BillingAccountProfileStateService;
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
intraprocessMessagingSubject: Subject<Message<object>>; intraprocessMessagingSubject: Subject<Message<Record<string, unknown>>>;
userAutoUnlockKeyService: UserAutoUnlockKeyService; userAutoUnlockKeyService: UserAutoUnlockKeyService;
scriptInjectorService: BrowserScriptInjectorService; scriptInjectorService: BrowserScriptInjectorService;
kdfConfigService: kdfConfigServiceAbstraction; kdfConfigService: kdfConfigServiceAbstraction;
@@ -384,7 +384,7 @@ export default class MainBackground {
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
this.storageService = new BrowserLocalStorageService(); this.storageService = new BrowserLocalStorageService();
this.intraprocessMessagingSubject = new Subject<Message<object>>(); this.intraprocessMessagingSubject = new Subject<Message<Record<string, unknown>>>();
this.messagingService = MessageSender.combine( this.messagingService = MessageSender.combine(
new SubjectMessageSender(this.intraprocessMessagingSubject), new SubjectMessageSender(this.intraprocessMessagingSubject),
@@ -840,7 +840,12 @@ export default class MainBackground {
this.authService, this.authService,
); );
this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener); this.syncServiceListener = new SyncServiceListener(
this.syncService,
messageListener,
this.messagingService,
this.logService,
);
} }
this.eventUploadService = new EventUploadService( this.eventUploadService = new EventUploadService(
this.apiService, this.apiService,
@@ -1170,7 +1175,7 @@ export default class MainBackground {
this.contextMenusBackground?.init(); this.contextMenusBackground?.init();
await this.idleBackground.init(); await this.idleBackground.init();
this.webRequestBackground?.startListening(); this.webRequestBackground?.startListening();
this.syncServiceListener?.startListening(); this.syncServiceListener?.listener$().subscribe();
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
setTimeout(async () => { setTimeout(async () => {

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.5.0", "version": "2024.5.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.5.0", "version": "2024.5.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -15,9 +15,9 @@ const HANDLED_ERRORS: Record<string, ErrorHandler> = {
export class ChromeMessageSender implements MessageSender { export class ChromeMessageSender implements MessageSender {
constructor(private readonly logService: LogService) {} constructor(private readonly logService: LogService) {}
send<T extends object>( send<T extends Record<string, unknown>>(
commandDefinition: string | CommandDefinition<T>, commandDefinition: string | CommandDefinition<T>,
payload: object | T = {}, payload: Record<string, unknown> | T = {},
): void { ): void {
const command = getCommand(commandDefinition); const command = getCommand(commandDefinition);
chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => { chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => {

View File

@@ -0,0 +1,130 @@
import { mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DO_FULL_SYNC, ForegroundSyncService, FullSyncMessage } from "./foreground-sync.service";
import { FullSyncFinishedMessage } from "./sync-service.listener";
describe("ForegroundSyncService", () => {
const stateService = mock<StateService>();
const folderService = mock<InternalFolderService>();
const folderApiService = mock<FolderApiServiceAbstraction>();
const messageSender = mock<MessageSender>();
const logService = mock<LogService>();
const cipherService = mock<CipherService>();
const collectionService = mock<CollectionService>();
const apiService = mock<ApiService>();
const accountService = mock<AccountService>();
const authService = mock<AuthService>();
const sendService = mock<InternalSendService>();
const sendApiService = mock<SendApiService>();
const messageListener = mock<MessageListener>();
const sut = new ForegroundSyncService(
stateService,
folderService,
folderApiService,
messageSender,
logService,
cipherService,
collectionService,
apiService,
accountService,
authService,
sendService,
sendApiService,
messageListener,
);
beforeEach(() => {
jest.resetAllMocks();
});
describe("fullSync", () => {
const getAndAssertRequestId = (doFullSyncMessage: Omit<FullSyncMessage, "requestId">) => {
expect(messageSender.send).toHaveBeenCalledWith(
DO_FULL_SYNC,
// We don't know the request id since that is created internally
expect.objectContaining(doFullSyncMessage),
);
const message = messageSender.send.mock.calls[0][1];
if (!("requestId" in message) || typeof message.requestId !== "string") {
throw new Error("requestId property of type string was expected on the sent message.");
}
return message.requestId;
};
it("correctly relays a successful fullSync", async () => {
const messages = new Subject<FullSyncFinishedMessage>();
messageListener.messages$.mockReturnValue(messages);
const fullSyncPromise = sut.fullSync(true, false);
expect(sut.syncInProgress).toBe(true);
const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: false });
// Pretend the sync has finished
messages.next({ successfully: true, errorMessage: null, requestId: requestId });
const result = await fullSyncPromise;
expect(sut.syncInProgress).toBe(false);
expect(result).toBe(true);
});
it("correctly relays an unsuccessful fullSync but does not throw if allowThrowOnError = false", async () => {
const messages = new Subject<FullSyncFinishedMessage>();
messageListener.messages$.mockReturnValue(messages);
const fullSyncPromise = sut.fullSync(false, false);
expect(sut.syncInProgress).toBe(true);
const requestId = getAndAssertRequestId({ forceSync: false, allowThrowOnError: false });
// Pretend the sync has finished
messages.next({
successfully: false,
errorMessage: "Error while syncing",
requestId: requestId,
});
const result = await fullSyncPromise;
expect(sut.syncInProgress).toBe(false);
expect(result).toBe(false);
});
it("correctly relays an unsuccessful fullSync but and will throw if allowThrowOnError = true", async () => {
const messages = new Subject<FullSyncFinishedMessage>();
messageListener.messages$.mockReturnValue(messages);
const fullSyncPromise = sut.fullSync(true, true);
expect(sut.syncInProgress).toBe(true);
const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: true });
// Pretend the sync has finished
messages.next({
successfully: false,
errorMessage: "Error while syncing",
requestId: requestId,
});
await expect(fullSyncPromise).rejects.toThrow("Error while syncing");
expect(sut.syncInProgress).toBe(false);
});
});
});

View File

@@ -1,4 +1,4 @@
import { firstValueFrom, timeout } from "rxjs"; import { filter, firstValueFrom, of, timeout } from "rxjs";
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";
@@ -10,6 +10,7 @@ import {
MessageListener, MessageListener,
MessageSender, MessageSender,
} from "@bitwarden/common/platform/messaging"; } from "@bitwarden/common/platform/messaging";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; import { CoreSyncService } from "@bitwarden/common/platform/sync/internal";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@@ -18,11 +19,11 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted"); import { FULL_SYNC_FINISHED } from "./sync-service.listener";
export const DO_FULL_SYNC = new CommandDefinition<{
forceSync: boolean; export type FullSyncMessage = { forceSync: boolean; allowThrowOnError: boolean; requestId: string };
allowThrowOnError: boolean;
}>("doFullSync"); export const DO_FULL_SYNC = new CommandDefinition<FullSyncMessage>("doFullSync");
export class ForegroundSyncService extends CoreSyncService { export class ForegroundSyncService extends CoreSyncService {
constructor( constructor(
@@ -59,18 +60,29 @@ export class ForegroundSyncService extends CoreSyncService {
async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise<boolean> { async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise<boolean> {
this.syncInProgress = true; this.syncInProgress = true;
try { try {
const requestId = Utils.newGuid();
const syncCompletedPromise = firstValueFrom( const syncCompletedPromise = firstValueFrom(
this.messageListener.messages$(SYNC_COMPLETED).pipe( this.messageListener.messages$(FULL_SYNC_FINISHED).pipe(
filter((m) => m.requestId === requestId),
timeout({ timeout({
first: 10_000, first: 30_000,
// If we haven't heard back in 30 seconds, just pretend we heard back about an unsuccesful sync.
with: () => { with: () => {
throw new Error("Timeout while doing a fullSync call."); this.logService.warning(
"ForegroundSyncService did not receive a message back in a reasonable time.",
);
return of({ successfully: false, errorMessage: "Sync timed out." });
}, },
}), }),
), ),
); );
this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError }); this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError, requestId });
const result = await syncCompletedPromise; const result = await syncCompletedPromise;
if (allowThrowOnError && result.errorMessage != null) {
throw new Error(result.errorMessage);
}
return result.successfully; return result.successfully;
} finally { } finally {
this.syncInProgress = false; this.syncInProgress = false;

View File

@@ -0,0 +1,60 @@
import { mock } from "jest-mock-extended";
import { Subject, firstValueFrom } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
import { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { FullSyncMessage } from "./foreground-sync.service";
import { FULL_SYNC_FINISHED, SyncServiceListener } from "./sync-service.listener";
describe("SyncServiceListener", () => {
const syncService = mock<SyncService>();
const messageListener = mock<MessageListener>();
const messageSender = mock<MessageSender>();
const logService = mock<LogService>();
const messages = new Subject<FullSyncMessage>();
messageListener.messages$.mockReturnValue(messages.asObservable().pipe(tagAsExternal()));
const sut = new SyncServiceListener(syncService, messageListener, messageSender, logService);
describe("listener$", () => {
it.each([true, false])(
"calls full sync and relays outcome when sync is [successfully = %s]",
async (value) => {
const listener = sut.listener$();
const emissionPromise = firstValueFrom(listener);
syncService.fullSync.mockResolvedValueOnce(value);
messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" });
await emissionPromise;
expect(syncService.fullSync).toHaveBeenCalledWith(true, false);
expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, {
successfully: value,
errorMessage: null,
requestId: "1",
});
},
);
it("calls full sync and relays error message through messaging", async () => {
const listener = sut.listener$();
const emissionPromise = firstValueFrom(listener);
syncService.fullSync.mockRejectedValueOnce(new Error("SyncError"));
messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" });
await emissionPromise;
expect(syncService.fullSync).toHaveBeenCalledWith(true, false);
expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, {
successfully: false,
errorMessage: "SyncError",
requestId: "1",
});
});
});
});

View File

@@ -1,25 +1,58 @@
import { Subscription, concatMap, filter } from "rxjs"; import { Observable, concatMap, filter } from "rxjs";
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
CommandDefinition,
MessageListener,
MessageSender,
isExternalMessage,
} from "@bitwarden/common/platform/messaging";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DO_FULL_SYNC } from "./foreground-sync.service"; import { DO_FULL_SYNC } from "./foreground-sync.service";
export type FullSyncFinishedMessage = {
successfully: boolean;
errorMessage: string;
requestId: string;
};
export const FULL_SYNC_FINISHED = new CommandDefinition<FullSyncFinishedMessage>(
"fullSyncFinished",
);
export class SyncServiceListener { export class SyncServiceListener {
constructor( constructor(
private readonly syncService: SyncService, private readonly syncService: SyncService,
private readonly messageListener: MessageListener, private readonly messageListener: MessageListener,
private readonly messageSender: MessageSender,
private readonly logService: LogService,
) {} ) {}
startListening(): Subscription { listener$(): Observable<void> {
return this.messageListener return this.messageListener.messages$(DO_FULL_SYNC).pipe(
.messages$(DO_FULL_SYNC) filter((message) => isExternalMessage(message)),
.pipe( concatMap(async ({ forceSync, allowThrowOnError, requestId }) => {
filter((message) => isExternalMessage(message)), await this.doFullSync(forceSync, allowThrowOnError, requestId);
concatMap(async ({ forceSync, allowThrowOnError }) => { }),
await this.syncService.fullSync(forceSync, allowThrowOnError); );
}), }
)
.subscribe(); private async doFullSync(forceSync: boolean, allowThrowOnError: boolean, requestId: string) {
try {
const result = await this.syncService.fullSync(forceSync, allowThrowOnError);
this.messageSender.send(FULL_SYNC_FINISHED, {
successfully: result,
errorMessage: null,
requestId,
});
} catch (err) {
this.logService.warning("Error while doing full sync in SyncServiceListener", err);
this.messageSender.send(FULL_SYNC_FINISHED, {
successfully: false,
errorMessage: err?.message ?? "Unknown Sync Error",
requestId,
});
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { map, share } from "rxjs"; import { map, share } from "rxjs";
import { Message } from "@bitwarden/common/platform/messaging";
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
import { fromChromeEvent } from "../browser/from-chrome-event"; import { fromChromeEvent } from "../browser/from-chrome-event";
@@ -20,7 +21,7 @@ export const fromChromeRuntimeMessaging = () => {
return message; return message;
}), }),
tagAsExternal, tagAsExternal<Message<Record<string, unknown>>>(),
share(), share(),
); );
}; };

View File

@@ -524,7 +524,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: MessageListener, provide: MessageListener,
useFactory: (subject: Subject<Message<object>>, ngZone: NgZone) => useFactory: (subject: Subject<Message<Record<string, unknown>>>, ngZone: NgZone) =>
new MessageListener( new MessageListener(
merge( merge(
subject.asObservable(), // For messages in the same context subject.asObservable(), // For messages in the same context
@@ -535,7 +535,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: MessageSender, provide: MessageSender,
useFactory: (subject: Subject<Message<object>>, logService: LogService) => useFactory: (subject: Subject<Message<Record<string, unknown>>>, logService: LogService) =>
MessageSender.combine( MessageSender.combine(
new SubjectMessageSender(subject), // For sending messages in the same context new SubjectMessageSender(subject), // For sending messages in the same context
new ChromeMessageSender(logService), // For sending messages to different contexts new ChromeMessageSender(logService), // For sending messages to different contexts
@@ -550,14 +550,14 @@ const safeProviders: SafeProvider[] = [
// we need the same instance that our in memory background is utilizing. // we need the same instance that our in memory background is utilizing.
return getBgService("intraprocessMessagingSubject")(); return getBgService("intraprocessMessagingSubject")();
} else { } else {
return new Subject<Message<object>>(); return new Subject<Message<Record<string, unknown>>>();
} }
}, },
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({
provide: MessageSender, provide: MessageSender,
useFactory: (subject: Subject<Message<object>>, logService: LogService) => useFactory: (subject: Subject<Message<Record<string, unknown>>>, logService: LogService) =>
MessageSender.combine( MessageSender.combine(
new SubjectMessageSender(subject), // For sending messages in the same context new SubjectMessageSender(subject), // For sending messages in the same context
new ChromeMessageSender(logService), // For sending messages to different contexts new ChromeMessageSender(logService), // For sending messages to different contexts
@@ -576,7 +576,7 @@ const safeProviders: SafeProvider[] = [
// There isn't a locally created background so we will communicate with // There isn't a locally created background so we will communicate with
// the true background through chrome apis, in that case, we can just create // the true background through chrome apis, in that case, we can just create
// one for ourself. // one for ourself.
return new Subject<Message<object>>(); return new Subject<Message<Record<string, unknown>>>();
} }
}, },
deps: [], deps: [],

View File

@@ -0,0 +1,8 @@
<div class="tw-mb-2">
<bit-search
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"
>
</bit-search>
</div>

View File

@@ -0,0 +1,35 @@
import { CommonModule } from "@angular/common";
import { Component, Output, EventEmitter } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { Subject, debounceTime } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchModule } from "@bitwarden/components";
const SearchTextDebounceInterval = 200;
@Component({
imports: [CommonModule, SearchModule, JslibModule, FormsModule],
standalone: true,
selector: "app-vault-v2-search",
templateUrl: "vault-v2-search.component.html",
})
export class VaultV2SearchComponent {
searchText: string;
@Output() searchTextChanged = new EventEmitter<string>();
private searchText$ = new Subject<string>();
constructor() {
this.searchText$
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
.subscribe((data) => {
this.searchTextChanged.emit(data);
});
}
onSearchTextChanged() {
this.searchText$.next(this.searchText);
}
}

View File

@@ -24,6 +24,9 @@
<ng-container *ngIf="!(showEmptyState$ | async)"> <ng-container *ngIf="!(showEmptyState$ | async)">
<!-- TODO: Filter/search Section in PM-6824 and PM-6826.--> <!-- TODO: Filter/search Section in PM-6824 and PM-6826.-->
<app-vault-v2-search (searchTextChanged)="handleSearchTextChange($event)">
</app-vault-v2-search>
<div <div
*ngIf="showNoResultsState$ | async" *ngIf="showNoResultsState$ | async"
class="tw-flex tw-flex-col tw-h-full tw-justify-center" class="tw-flex tw-flex-col tw-h-full tw-justify-center"

View File

@@ -11,6 +11,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
@Component({ @Component({
selector: "app-vault", selector: "app-vault",
@@ -28,6 +29,7 @@ import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } fro
VaultListItemsContainerComponent, VaultListItemsContainerComponent,
ButtonModule, ButtonModule,
RouterLink, RouterLink,
VaultV2SearchComponent,
], ],
}) })
export class VaultV2Component implements OnInit, OnDestroy { export class VaultV2Component implements OnInit, OnDestroy {
@@ -48,6 +50,10 @@ export class VaultV2Component implements OnInit, OnDestroy {
ngOnDestroy(): void {} ngOnDestroy(): void {}
handleSearchTextChange(searchText: string) {
this.vaultPopupItemsService.applyFilter(searchText);
}
addCipher() { addCipher() {
// TODO: Add currently filtered organization to query params if available // TODO: Add currently filtered organization to query params if available
void this.router.navigate(["/add-cipher"], {}); void this.router.navigate(["/add-cipher"], {});

View File

@@ -1,6 +1,7 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.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 { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
@@ -19,6 +20,7 @@ describe("VaultPopupItemsService", () => {
const cipherServiceMock = mock<CipherService>(); const cipherServiceMock = mock<CipherService>();
const vaultSettingsServiceMock = mock<VaultSettingsService>(); const vaultSettingsServiceMock = mock<VaultSettingsService>();
const searchService = mock<SearchService>();
beforeEach(() => { beforeEach(() => {
allCiphers = cipherFactory(10); allCiphers = cipherFactory(10);
@@ -34,6 +36,7 @@ describe("VaultPopupItemsService", () => {
cipherList[3].favorite = true; cipherList[3].favorite = true;
cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable(); cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
searchService.searchCiphers.mockImplementation(async () => cipherList);
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable(); vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable(); vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
@@ -41,11 +44,19 @@ describe("VaultPopupItemsService", () => {
jest jest
.spyOn(BrowserApi, "getTabFromCurrentWindow") .spyOn(BrowserApi, "getTabFromCurrentWindow")
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab); .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock); service = new VaultPopupItemsService(
cipherServiceMock,
vaultSettingsServiceMock,
searchService,
);
}); });
it("should be created", () => { it("should be created", () => {
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock); service = new VaultPopupItemsService(
cipherServiceMock,
vaultSettingsServiceMock,
searchService,
);
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
@@ -73,7 +84,11 @@ describe("VaultPopupItemsService", () => {
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable(); vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock); service = new VaultPopupItemsService(
cipherServiceMock,
vaultSettingsServiceMock,
searchService,
);
service.autoFillCiphers$.subscribe((ciphers) => { service.autoFillCiphers$.subscribe((ciphers) => {
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1); expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
@@ -99,7 +114,11 @@ describe("VaultPopupItemsService", () => {
Object.values(allCiphers), Object.values(allCiphers),
); );
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock); service = new VaultPopupItemsService(
cipherServiceMock,
vaultSettingsServiceMock,
searchService,
);
service.autoFillCiphers$.subscribe((ciphers) => { service.autoFillCiphers$.subscribe((ciphers) => {
expect(ciphers.length).toBe(10); expect(ciphers.length).toBe(10);
@@ -114,6 +133,24 @@ describe("VaultPopupItemsService", () => {
done(); done();
}); });
}); });
it("should filter autoFillCiphers$ down to search term", (done) => {
const cipherList = Object.values(allCiphers);
const searchText = "Login";
searchService.searchCiphers.mockImplementation(async () => {
return cipherList.filter((cipher) => {
return cipher.name.includes(searchText);
});
});
// there is only 1 Login returned for filteredCiphers. but two results expected because of other autofill types
service.autoFillCiphers$.subscribe((ciphers) => {
expect(ciphers[0].name.includes(searchText)).toBe(true);
expect(ciphers.length).toBe(2);
done();
});
});
}); });
describe("favoriteCiphers$", () => { describe("favoriteCiphers$", () => {
@@ -131,6 +168,24 @@ describe("VaultPopupItemsService", () => {
done(); done();
}); });
}); });
it("should filter favoriteCiphers$ down to search term", (done) => {
const cipherList = Object.values(allCiphers);
const searchText = "Card 2";
searchService.searchCiphers.mockImplementation(async () => {
return cipherList.filter((cipher) => {
return cipher.name === searchText;
});
});
service.favoriteCiphers$.subscribe((ciphers) => {
// There are 2 favorite items but only one Card 2
expect(ciphers[0].name).toBe(searchText);
expect(ciphers.length).toBe(1);
done();
});
});
}); });
describe("remainingCiphers$", () => { describe("remainingCiphers$", () => {
@@ -148,12 +203,33 @@ describe("VaultPopupItemsService", () => {
done(); done();
}); });
}); });
it("should filter remainingCiphers$ down to search term", (done) => {
const cipherList = Object.values(allCiphers);
const searchText = "Login";
searchService.searchCiphers.mockImplementation(async () => {
return cipherList.filter((cipher) => {
return cipher.name.includes(searchText);
});
});
service.remainingCiphers$.subscribe((ciphers) => {
// There are 6 remaining ciphers but only 2 with "Login" in the name
expect(ciphers.length).toBe(2);
done();
});
});
}); });
describe("emptyVault$", () => { describe("emptyVault$", () => {
it("should return true if there are no ciphers", (done) => { it("should return true if there are no ciphers", (done) => {
cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable(); cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock); service = new VaultPopupItemsService(
cipherServiceMock,
vaultSettingsServiceMock,
searchService,
);
service.emptyVault$.subscribe((empty) => { service.emptyVault$.subscribe((empty) => {
expect(empty).toBe(true); expect(empty).toBe(true);
done(); done();
@@ -192,6 +268,54 @@ describe("VaultPopupItemsService", () => {
}); });
}); });
}); });
describe("noFilteredResults$", () => {
it("should return false when filteredResults has values", (done) => {
service.noFilteredResults$.subscribe((noResults) => {
expect(noResults).toBe(false);
done();
});
});
it("should return true when there are zero filteredResults", (done) => {
searchService.searchCiphers.mockImplementation(async () => []);
service.noFilteredResults$.subscribe((noResults) => {
expect(noResults).toBe(true);
done();
});
});
});
describe("hasFilterApplied$", () => {
it("should return true if the search term provided is searchable", (done) => {
searchService.isSearchable.mockImplementation(async () => true);
service.hasFilterApplied$.subscribe((canSearch) => {
expect(canSearch).toBe(true);
done();
});
});
it("should return false if the search term provided is not searchable", (done) => {
searchService.isSearchable.mockImplementation(async () => false);
service.hasFilterApplied$.subscribe((canSearch) => {
expect(canSearch).toBe(false);
done();
});
});
});
describe("applyFilter", () => {
it("should call search Service with the new search term", (done) => {
const searchText = "Hello";
service.applyFilter(searchText);
const searchServiceSpy = jest.spyOn(searchService, "searchCiphers");
service.favoriteCiphers$.subscribe(() => {
expect(searchServiceSpy).toHaveBeenCalledWith(searchText, null, expect.anything());
done();
});
});
});
}); });
// A function to generate a list of ciphers of different types // A function to generate a list of ciphers of different types

View File

@@ -1,5 +1,6 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { import {
BehaviorSubject,
combineLatest, combineLatest,
map, map,
Observable, Observable,
@@ -10,6 +11,7 @@ import {
switchMap, switchMap,
} from "rxjs"; } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -26,6 +28,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
}) })
export class VaultPopupItemsService { export class VaultPopupItemsService {
private _refreshCurrentTab$ = new Subject<void>(); private _refreshCurrentTab$ = new Subject<void>();
private searchText$ = new BehaviorSubject<string>("");
/** /**
* Observable that contains the list of other cipher types that should be shown * Observable that contains the list of other cipher types that should be shown
@@ -69,6 +72,13 @@ export class VaultPopupItemsService {
shareReplay({ refCount: false, bufferSize: 1 }), shareReplay({ refCount: false, bufferSize: 1 }),
); );
private _filteredCipherList$ = combineLatest([this._cipherList$, this.searchText$]).pipe(
switchMap(([ciphers, searchText]) =>
this.searchService.searchCiphers(searchText, null, ciphers),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
/** /**
* List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities * List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities
* if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name. * if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name.
@@ -76,7 +86,7 @@ export class VaultPopupItemsService {
* See {@link refreshCurrentTab} to trigger re-evaluation of the current tab. * See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
*/ */
autoFillCiphers$: Observable<CipherView[]> = combineLatest([ autoFillCiphers$: Observable<CipherView[]> = combineLatest([
this._cipherList$, this._filteredCipherList$,
this._otherAutoFillTypes$, this._otherAutoFillTypes$,
this._currentAutofillTab$, this._currentAutofillTab$,
]).pipe( ]).pipe(
@@ -96,7 +106,7 @@ export class VaultPopupItemsService {
*/ */
favoriteCiphers$: Observable<CipherView[]> = combineLatest([ favoriteCiphers$: Observable<CipherView[]> = combineLatest([
this.autoFillCiphers$, this.autoFillCiphers$,
this._cipherList$, this._filteredCipherList$,
]).pipe( ]).pipe(
map(([autoFillCiphers, ciphers]) => map(([autoFillCiphers, ciphers]) =>
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)), ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
@@ -114,7 +124,7 @@ export class VaultPopupItemsService {
remainingCiphers$: Observable<CipherView[]> = combineLatest([ remainingCiphers$: Observable<CipherView[]> = combineLatest([
this.autoFillCiphers$, this.autoFillCiphers$,
this.favoriteCiphers$, this.favoriteCiphers$,
this._cipherList$, this._filteredCipherList$,
]).pipe( ]).pipe(
map(([autoFillCiphers, favoriteCiphers, ciphers]) => map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
ciphers.filter( ciphers.filter(
@@ -129,7 +139,9 @@ export class VaultPopupItemsService {
* Observable that indicates whether a filter is currently applied to the ciphers. * Observable that indicates whether a filter is currently applied to the ciphers.
* @todo Implement filter/search functionality in PM-6824 and PM-6826. * @todo Implement filter/search functionality in PM-6824 and PM-6826.
*/ */
hasFilterApplied$: Observable<boolean> = of(false); hasFilterApplied$: Observable<boolean> = this.searchText$.pipe(
switchMap((text) => this.searchService.isSearchable(text)),
);
/** /**
* Observable that indicates whether autofill is allowed in the current context. * Observable that indicates whether autofill is allowed in the current context.
@@ -146,11 +158,14 @@ export class VaultPopupItemsService {
* Observable that indicates whether there are no ciphers to show with the current filter. * Observable that indicates whether there are no ciphers to show with the current filter.
* @todo Implement filter/search functionality in PM-6824 and PM-6826. * @todo Implement filter/search functionality in PM-6824 and PM-6826.
*/ */
noFilteredResults$: Observable<boolean> = of(false); noFilteredResults$: Observable<boolean> = this._filteredCipherList$.pipe(
map((ciphers) => !ciphers.length),
);
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private searchService: SearchService,
) {} ) {}
/** /**
@@ -160,6 +175,10 @@ export class VaultPopupItemsService {
this._refreshCurrentTab$.next(null); this._refreshCurrentTab$.next(null);
} }
applyFilter(newSearchText: string) {
this.searchText$.next(newSearchText);
}
/** /**
* Sort function for ciphers to be used in the autofill section of the Vault tab. * Sort function for ciphers to be used in the autofill section of the Vault tab.
* Sorts by type, then by last used date, and finally by name. * Sorts by type, then by last used date, and finally by name.

View File

@@ -26,6 +26,7 @@
"@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/vault": ["../../libs/vault/src"] "@bitwarden/vault": ["../../libs/vault/src"]
}, },
"useDefineForClassFields": false "useDefineForClassFields": false

View File

@@ -151,7 +151,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: MessageSender, provide: MessageSender,
useFactory: (subject: Subject<Message<object>>) => useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
MessageSender.combine( MessageSender.combine(
new ElectronRendererMessageSender(), // Communication with main process new ElectronRendererMessageSender(), // Communication with main process
new SubjectMessageSender(subject), // Communication with ourself new SubjectMessageSender(subject), // Communication with ourself
@@ -160,7 +160,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: MessageListener, provide: MessageListener,
useFactory: (subject: Subject<Message<object>>) => useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
new MessageListener( new MessageListener(
merge( merge(
subject.asObservable(), // For messages from the same context subject.asObservable(), // For messages from the same context

View File

@@ -223,7 +223,7 @@ export class Main {
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
const messageSubject = new Subject<Message<object>>(); const messageSubject = new Subject<Message<Record<string, unknown>>>();
this.messagingService = MessageSender.combine( this.messagingService = MessageSender.combine(
new SubjectMessageSender(messageSubject), // For local messages new SubjectMessageSender(messageSubject), // For local messages
new ElectronMainMessagingService(this.windowMain), new ElectronMainMessagingService(this.windowMain),

View File

@@ -140,7 +140,7 @@ export class TrayMain {
} }
updateContextMenu() { updateContextMenu() {
if (this.contextMenu != null && this.isLinux()) { if (this.tray != null && this.contextMenu != null && this.isLinux()) {
this.tray.setContextMenu(this.contextMenu); this.tray.setContextMenu(this.contextMenu);
} }
} }

View File

@@ -2,9 +2,9 @@ import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/mes
import { getCommand } from "@bitwarden/common/platform/messaging/internal"; import { getCommand } from "@bitwarden/common/platform/messaging/internal";
export class ElectronRendererMessageSender implements MessageSender { export class ElectronRendererMessageSender implements MessageSender {
send<T extends object>( send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T> | string, commandDefinition: CommandDefinition<T> | string,
payload: object | T = {}, payload: Record<string, unknown> | T = {},
): void { ): void {
const command = getCommand(commandDefinition); const command = getCommand(commandDefinition);
ipc.platform.sendMessage(Object.assign({}, { command: command }, payload)); ipc.platform.sendMessage(Object.assign({}, { command: command }, payload));

View File

@@ -8,8 +8,8 @@ import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
* @returns An observable stream of messages. * @returns An observable stream of messages.
*/ */
export const fromIpcMessaging = () => { export const fromIpcMessaging = () => {
return fromEventPattern<Message<object>>( return fromEventPattern<Message<Record<string, unknown>>>(
(handler) => ipc.platform.onMessage.addListener(handler), (handler) => ipc.platform.onMessage.addListener(handler),
(handler) => ipc.platform.onMessage.removeListener(handler), (handler) => ipc.platform.onMessage.removeListener(handler),
).pipe(tagAsExternal, share()); ).pipe(tagAsExternal(), share());
}; };

View File

@@ -87,7 +87,10 @@ export class ElectronMainMessagingService implements MessageSender {
}); });
} }
send<T extends object>(commandDefinition: CommandDefinition<T> | string, arg: T | object = {}) { send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T> | string,
arg: T | Record<string, unknown> = {},
) {
const command = getCommand(commandDefinition); const command = getCommand(commandDefinition);
const message = Object.assign({}, { command: command }, arg); const message = Object.assign({}, { command: command }, arg);
if (this.windowMain.win != null) { if (this.windowMain.win != null) {

View File

@@ -25,6 +25,7 @@
"@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/node/*": ["../../libs/node/src/*"], "@bitwarden/node/*": ["../../libs/node/src/*"],
"@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/vault": ["../../libs/vault/src"] "@bitwarden/vault": ["../../libs/vault/src"]
}, },
"useDefineForClassFields": false "useDefineForClassFields": false

View File

@@ -7,8 +7,8 @@
<ng-container bitDialogContent> <ng-container bitDialogContent>
<app-user-verification-form-input <app-user-verification-form-input
formControlName="secret" formControlName="secret"
ngDefaultControl
name="secret" name="secret"
[(invalidSecret)]="invalidSecret"
></app-user-verification-form-input> ></app-user-verification-form-input>
</ng-container> </ng-container>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>

View File

@@ -10,6 +10,7 @@ import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response"; import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
import { Verification } from "@bitwarden/common/auth/types/verification"; import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -32,6 +33,7 @@ export class TwoFactorVerifyComponent {
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
secret: new FormControl<Verification | null>(null), secret: new FormControl<Verification | null>(null),
}); });
invalidSecret: boolean = false;
constructor( constructor(
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData, @Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
@@ -45,23 +47,30 @@ export class TwoFactorVerifyComponent {
} }
submit = async () => { submit = async () => {
let hashedSecret: string; try {
this.formPromise = this.userVerificationService let hashedSecret: string;
.buildRequest(this.formGroup.value.secret) this.formPromise = this.userVerificationService
.then((request) => { .buildRequest(this.formGroup.value.secret)
hashedSecret = .then((request) => {
this.formGroup.value.secret.type === VerificationType.MasterPassword hashedSecret =
? request.masterPasswordHash this.formGroup.value.secret.type === VerificationType.MasterPassword
: request.otp; ? request.masterPasswordHash
return this.apiCall(request); : request.otp;
}); return this.apiCall(request);
});
const response = await this.formPromise; const response = await this.formPromise;
this.dialogRef.close({ this.dialogRef.close({
response: response, response: response,
secret: hashedSecret, secret: hashedSecret,
verificationType: this.formGroup.value.secret.type, verificationType: this.formGroup.value.secret.type,
}); });
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
this.invalidSecret = true;
}
throw e;
}
}; };
get dialogTitle(): string { get dialogTitle(): string {

View File

@@ -19,6 +19,7 @@
"@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["src/*"] "@bitwarden/web-vault/*": ["src/*"]
} }

View File

@@ -15,6 +15,7 @@
"../../libs/tools/export/vault-export/vault-export-core/src" "../../libs/tools/export/vault-export/vault-export-core/src"
], ],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"], "@bitwarden/web-vault/*": ["../../apps/web/src/*"],

View File

@@ -19,6 +19,7 @@
"@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/platform": ["../../libs/platform/src"],
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/vault": ["../../libs/vault/src"],
"@bitwarden/web-vault/*": ["../../apps/web/src/*"], "@bitwarden/web-vault/*": ["../../apps/web/src/*"],

View File

@@ -49,7 +49,7 @@ export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeTy
"SYSTEM_THEME_OBSERVABLE", "SYSTEM_THEME_OBSERVABLE",
); );
export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT"); export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT");
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>( export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<
"INTRAPROCESS_MESSAGING_SUBJECT", Subject<Message<Record<string, unknown>>>
); >("INTRAPROCESS_MESSAGING_SUBJECT");
export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE"); export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE");

View File

@@ -649,7 +649,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: BroadcasterService, provide: BroadcasterService,
useClass: DefaultBroadcasterService, useClass: DefaultBroadcasterService,
deps: [MessageSender, MessageListener], deps: [MessageListener],
}), }),
safeProvider({ safeProvider({
provide: VaultTimeoutSettingsServiceAbstraction, provide: VaultTimeoutSettingsServiceAbstraction,
@@ -1165,17 +1165,19 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT, provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => new Subject<Message<object>>(), useFactory: () => new Subject<Message<Record<string, unknown>>>(),
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({
provide: MessageListener, provide: MessageListener,
useFactory: (subject: Subject<Message<object>>) => new MessageListener(subject.asObservable()), useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
new MessageListener(subject.asObservable()),
deps: [INTRAPROCESS_MESSAGING_SUBJECT], deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}), }),
safeProvider({ safeProvider({
provide: MessageSender, provide: MessageSender,
useFactory: (subject: Subject<Message<object>>) => new SubjectMessageSender(subject), useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
new SubjectMessageSender(subject),
deps: [INTRAPROCESS_MESSAGING_SUBJECT], deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}), }),
safeProvider({ safeProvider({

View File

@@ -6,10 +6,6 @@ export interface MessageBase {
* @deprecated Use the observable from the appropriate service instead. * @deprecated Use the observable from the appropriate service instead.
*/ */
export abstract class BroadcasterService { export abstract class BroadcasterService {
/**
* @deprecated Use the observable from the appropriate service instead.
*/
abstract send(message: MessageBase, id?: string): void;
/** /**
* @deprecated Use the observable from the appropriate service instead. * @deprecated Use the observable from the appropriate service instead.
*/ */

View File

@@ -12,7 +12,7 @@ describe("helpers", () => {
}); });
it("can get the command from a message definition", () => { it("can get the command from a message definition", () => {
const commandDefinition = new CommandDefinition<object>("myCommand"); const commandDefinition = new CommandDefinition<Record<string, unknown>>("myCommand");
const command = getCommand(commandDefinition); const command = getCommand(commandDefinition);
@@ -22,9 +22,9 @@ describe("helpers", () => {
describe("tag integration", () => { describe("tag integration", () => {
it("can tag and identify as tagged", async () => { it("can tag and identify as tagged", async () => {
const messagesSubject = new Subject<Message<object>>(); const messagesSubject = new Subject<Message<Record<string, unknown>>>();
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal); const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal());
const firstValuePromise = firstValueFrom(taggedMessages); const firstValuePromise = firstValueFrom(taggedMessages);
@@ -39,7 +39,7 @@ describe("helpers", () => {
describe("isExternalMessage", () => { describe("isExternalMessage", () => {
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message< it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
Record<string, unknown> Record<string, unknown>
>[])("returns false when value is %s", (value: Message<object>) => { >[])("returns false when value is %s", (value: Message<Record<string, unknown>>) => {
expect(isExternalMessage(value)).toBe(false); expect(isExternalMessage(value)).toBe(false);
}); });
}); });

View File

@@ -1,8 +1,10 @@
import { MonoTypeOperatorFunction, map } from "rxjs"; import { map } from "rxjs";
import { Message, CommandDefinition } from "./types"; import { CommandDefinition } from "./types";
export const getCommand = (commandDefinition: CommandDefinition<object> | string) => { export const getCommand = (
commandDefinition: CommandDefinition<Record<string, unknown>> | string,
) => {
if (typeof commandDefinition === "string") { if (typeof commandDefinition === "string") {
return commandDefinition; return commandDefinition;
} else { } else {
@@ -16,8 +18,8 @@ export const isExternalMessage = (message: Record<PropertyKey, unknown>) => {
return message?.[EXTERNAL_SOURCE_TAG] === true; return message?.[EXTERNAL_SOURCE_TAG] === true;
}; };
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map( export const tagAsExternal = <T extends Record<PropertyKey, unknown>>() => {
(message: Message<object>) => { return map((message: T) => {
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
}, });
); };

View File

@@ -11,7 +11,7 @@ import { Message, CommandDefinition } from "./types";
* or vault data changes and those observables should be preferred over messaging. * or vault data changes and those observables should be preferred over messaging.
*/ */
export class MessageListener { export class MessageListener {
constructor(private readonly messageStream: Observable<Message<object>>) {} constructor(private readonly messageStream: Observable<Message<Record<string, unknown>>>) {}
/** /**
* A stream of all messages sent through the application. It does not contain type information for the * A stream of all messages sent through the application. It does not contain type information for the
@@ -28,7 +28,9 @@ export class MessageListener {
* *
* @param commandDefinition The CommandDefinition containing the information about the message type you care about. * @param commandDefinition The CommandDefinition containing the information about the message type you care about.
*/ */
messages$<T extends object>(commandDefinition: CommandDefinition<T>): Observable<T> { messages$<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T>,
): Observable<T> {
return this.allMessages$.pipe( return this.allMessages$.pipe(
filter((msg) => msg?.command === commandDefinition.command), filter((msg) => msg?.command === commandDefinition.command),
) as Observable<T>; ) as Observable<T>;

View File

@@ -3,9 +3,9 @@ import { CommandDefinition } from "./types";
class MultiMessageSender implements MessageSender { class MultiMessageSender implements MessageSender {
constructor(private readonly innerMessageSenders: MessageSender[]) {} constructor(private readonly innerMessageSenders: MessageSender[]) {}
send<T extends object>( send<T extends Record<string, unknown>>(
commandDefinition: string | CommandDefinition<T>, commandDefinition: string | CommandDefinition<T>,
payload: object | T = {}, payload: Record<string, unknown> | T = {},
): void { ): void {
for (const messageSender of this.innerMessageSenders) { for (const messageSender of this.innerMessageSenders) {
messageSender.send(commandDefinition, payload); messageSender.send(commandDefinition, payload);
@@ -26,7 +26,10 @@ export abstract class MessageSender {
* @param commandDefinition * @param commandDefinition
* @param payload * @param payload
*/ */
abstract send<T extends object>(commandDefinition: CommandDefinition<T>, payload: T): void; abstract send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T>,
payload: T,
): void;
/** /**
* A legacy method for sending messages in a non-type safe way. * A legacy method for sending messages in a non-type safe way.
@@ -38,12 +41,12 @@ export abstract class MessageSender {
* @param payload Extra contextual information regarding the message. Be aware that this payload may * @param payload Extra contextual information regarding the message. Be aware that this payload may
* be serialized and lose all prototype information. * be serialized and lose all prototype information.
*/ */
abstract send(command: string, payload?: object): void; abstract send(command: string, payload?: Record<string, unknown>): void;
/** Implementation of the other two overloads, read their docs instead. */ /** Implementation of the other two overloads, read their docs instead. */
abstract send<T extends object>( abstract send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T> | string, commandDefinition: CommandDefinition<T> | string,
payload: T | object, payload: T | Record<string, unknown>,
): void; ): void;
/** /**

View File

@@ -5,11 +5,11 @@ import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types"; import { Message, CommandDefinition } from "./types";
export class SubjectMessageSender implements MessageSender { export class SubjectMessageSender implements MessageSender {
constructor(private readonly messagesSubject: Subject<Message<object>>) {} constructor(private readonly messagesSubject: Subject<Message<Record<string, unknown>>>) {}
send<T extends object>( send<T extends Record<string, unknown>>(
commandDefinition: string | CommandDefinition<T>, commandDefinition: string | CommandDefinition<T>,
payload: object | T = {}, payload: Record<string, unknown> | T = {},
): void { ): void {
const command = getCommand(commandDefinition); const command = getCommand(commandDefinition);
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command })); this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));

View File

@@ -5,9 +5,9 @@ declare const tag: unique symbol;
* alonside `MessageSender` and `MessageListener` for providing a type * alonside `MessageSender` and `MessageListener` for providing a type
* safe(-ish) way of sending and receiving messages. * safe(-ish) way of sending and receiving messages.
*/ */
export class CommandDefinition<T extends object> { export class CommandDefinition<T extends Record<string, unknown>> {
[tag]: T; [tag]: T;
constructor(readonly command: string) {} constructor(readonly command: string) {}
} }
export type Message<T extends object> = { command: string } & T; export type Message<T extends Record<string, unknown>> = { command: string } & T;

View File

@@ -1,7 +1,7 @@
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service"; import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service";
import { MessageListener, MessageSender } from "../messaging"; import { MessageListener } from "../messaging";
/** /**
* Temporary implementation that just delegates to the message sender and message listener * Temporary implementation that just delegates to the message sender and message listener
@@ -10,14 +10,7 @@ import { MessageListener, MessageSender } from "../messaging";
export class DefaultBroadcasterService implements BroadcasterService { export class DefaultBroadcasterService implements BroadcasterService {
subscriptions = new Map<string, Subscription>(); subscriptions = new Map<string, Subscription>();
constructor( constructor(private readonly messageListener: MessageListener) {}
private readonly messageSender: MessageSender,
private readonly messageListener: MessageListener,
) {}
send(message: MessageBase, id?: string) {
this.messageSender.send(message?.command, message);
}
subscribe(id: string, messageCallback: (message: MessageBase) => void) { subscribe(id: string, messageCallback: (message: MessageBase) => void) {
this.subscriptions.set( this.subscriptions.set(

View File

@@ -33,6 +33,7 @@ export class Parser {
options: ParserOptions, options: ParserOptions,
): Promise<Account> { ): Promise<Account> {
let id: string; let id: string;
let step = 0;
try { try {
const placeholder = "decryption failed"; const placeholder = "decryption failed";
const reader = new BinaryReader(chunk.payload); const reader = new BinaryReader(chunk.payload);
@@ -42,6 +43,7 @@ export class Parser {
id = Utils.fromBufferToUtf8(this.readItem(reader)); id = Utils.fromBufferToUtf8(this.readItem(reader));
// 1: name // 1: name
step = 1;
const name = await this.cryptoUtils.decryptAes256PlainWithDefault( const name = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -49,6 +51,7 @@ export class Parser {
); );
// 2: group // 2: group
step = 2;
const group = await this.cryptoUtils.decryptAes256PlainWithDefault( const group = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -56,6 +59,7 @@ export class Parser {
); );
// 3: url // 3: url
step = 3;
let url = Utils.fromBufferToUtf8( let url = Utils.fromBufferToUtf8(
this.decodeHexLoose(Utils.fromBufferToUtf8(this.readItem(reader))), this.decodeHexLoose(Utils.fromBufferToUtf8(this.readItem(reader))),
); );
@@ -66,6 +70,7 @@ export class Parser {
} }
// 4: extra (notes) // 4: extra (notes)
step = 4;
const notes = await this.cryptoUtils.decryptAes256PlainWithDefault( const notes = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -73,12 +78,14 @@ export class Parser {
); );
// 5: fav (is favorite) // 5: fav (is favorite)
step = 5;
const isFavorite = Utils.fromBufferToUtf8(this.readItem(reader)) === "1"; const isFavorite = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
// 6: sharedfromaid (?) // 6: sharedfromaid (?)
this.skipItem(reader); this.skipItem(reader);
// 7: username // 7: username
step = 7;
let username = await this.cryptoUtils.decryptAes256PlainWithDefault( let username = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -86,6 +93,7 @@ export class Parser {
); );
// 8: password // 8: password
step = 8;
let password = await this.cryptoUtils.decryptAes256PlainWithDefault( let password = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -99,6 +107,7 @@ export class Parser {
this.skipItem(reader); this.skipItem(reader);
// 11: sn (is secure note) // 11: sn (is secure note)
step = 11;
const isSecureNote = Utils.fromBufferToUtf8(this.readItem(reader)) === "1"; const isSecureNote = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
// Parse secure note // Parse secure note
@@ -214,6 +223,7 @@ export class Parser {
this.skipItem(reader); this.skipItem(reader);
// 39: totp (?) // 39: totp (?)
step = 39;
const totp = await this.cryptoUtils.decryptAes256PlainWithDefault( const totp = await this.cryptoUtils.decryptAes256PlainWithDefault(
this.readItem(reader), this.readItem(reader),
encryptionKey, encryptionKey,
@@ -227,6 +237,7 @@ export class Parser {
// 42: last_credential_monitoring_stat (?) // 42: last_credential_monitoring_stat (?)
// Adjust the path to include the group and the shared folder, if any. // Adjust the path to include the group and the shared folder, if any.
step = 42;
const path = this.makeAccountPath(group, folder); const path = this.makeAccountPath(group, folder);
const account = new Account(); const account = new Account();
@@ -243,7 +254,12 @@ export class Parser {
return account; return account;
} catch (err) { } catch (err) {
throw new Error( throw new Error(
"Error parsing accounts on item with ID:" + id + " errorMessage: " + err.message, "Error parsing accounts on item with ID:" +
id +
" step #" +
step +
" errorMessage: " +
err.message,
); );
} }
} }

View File

@@ -15,6 +15,7 @@
"@bitwarden/importer/core": ["../importer/src"], "@bitwarden/importer/core": ["../importer/src"],
"@bitwarden/importer/ui": ["../importer/src/components"], "@bitwarden/importer/ui": ["../importer/src/components"],
"@bitwarden/platform": ["../platform/src"], "@bitwarden/platform": ["../platform/src"],
"@bitwarden/send-ui": ["../tools/send/send-ui/src"],
"@bitwarden/node/*": ["../node/src/*"], "@bitwarden/node/*": ["../node/src/*"],
"@bitwarden/vault": ["../vault/src"] "@bitwarden/vault": ["../vault/src"]
} }

View File

@@ -0,0 +1,100 @@
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</bit-callout>
<tools-export-scope-callout
[organizationId]="organizationId"
*ngIf="!disabledByPolicy"
></tools-export-scope-callout>
<form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm">
<ng-container *ngIf="organizations$ | async as organizations">
<bit-form-field *ngIf="organizations.length > 0">
<bit-label>{{ "exportFrom" | i18n }}</bit-label>
<bit-select formControlName="vaultSelector">
<bit-option [label]="'myVault' | i18n" value="myVault" icon="bwi-user" />
<bit-option
*ngFor="let o of organizations$ | async"
[value]="o.id"
[label]="o.name"
icon="bwi-business"
/>
</bit-select>
</bit-form-field>
</ng-container>
<bit-form-field>
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
<bit-select formControlName="format">
<bit-option *ngFor="let f of formatOptions" [value]="f.value" [label]="f.name" />
</bit-select>
</bit-form-field>
<ng-container *ngIf="format === 'encrypted_json'">
<bit-radio-group formControlName="fileEncryptionType" aria-label="exportTypeHeading">
<bit-label>{{ "exportTypeHeading" | i18n }}</bit-label>
<bit-radio-button
id="AccountEncrypted"
name="fileEncryptionType"
class="tw-block"
[value]="encryptedExportType.AccountEncrypted"
checked="fileEncryptionType === encryptedExportType.AccountEncrypted"
>
<bit-label>{{ "accountRestricted" | i18n }}</bit-label>
<bit-hint>{{ "accountRestrictedOptionDescription" | i18n }}</bit-hint>
</bit-radio-button>
<bit-radio-button
id="FileEncrypted"
name="fileEncryptionType"
class="tw-block"
[value]="encryptedExportType.FileEncrypted"
checked="fileEncryptionType === encryptedExportType.FileEncrypted"
>
<bit-label>{{ "passwordProtected" | i18n }}</bit-label>
<bit-hint>{{ "passwordProtectedOptionDescription" | i18n }}</bit-hint>
</bit-radio-button>
</bit-radio-group>
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "filePassword" | i18n }}</bit-label>
<input
bitInput
type="password"
id="filePassword"
formControlName="filePassword"
name="password"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showFilePassword"
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</bit-form-field>
<app-password-strength [password]="filePassword" [showText]="true"> </app-password-strength>
</div>
<bit-form-field>
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input
bitInput
type="password"
id="confirmFilePassword"
formControlName="confirmFilePassword"
name="confirmFilePassword"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showFilePassword"
></button>
</bit-form-field>
</ng-container>
</ng-container>
</form>

View File

@@ -1,7 +1,9 @@
import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; import { CommonModule } from "@angular/common";
import { UntypedFormBuilder, Validators } from "@angular/forms"; import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms";
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component"; import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@@ -16,11 +18,70 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum"; import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum";
import { DialogService } from "@bitwarden/components"; import {
AsyncActionsModule,
BitSubmitDirective,
ButtonModule,
CalloutModule,
DialogService,
FormFieldModule,
IconButtonModule,
RadioButtonModule,
SelectModule,
} from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
@Directive() import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
@Component({
selector: "tools-export",
templateUrl: "export.component.html",
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
JslibModule,
FormFieldModule,
AsyncActionsModule,
ButtonModule,
IconButtonModule,
SelectModule,
CalloutModule,
RadioButtonModule,
ExportScopeCalloutComponent,
UserVerificationDialogComponent,
],
})
export class ExportComponent implements OnInit, OnDestroy { export class ExportComponent implements OnInit, OnDestroy {
/**
* The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method.
* This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state.
*/
@ViewChild(BitSubmitDirective)
private bitSubmit: BitSubmitDirective;
/**
* Emits true when the BitSubmitDirective({@link bitSubmit} is executing {@link submit} and false when execution has completed.
* Example: Used to show the loading state of the submit button present on the hosting component
* */
@Output()
formLoading = new EventEmitter<boolean>();
/**
* Emits true when this form gets disabled and false when enabled.
* Example: Used to disable the submit button, which is present on the hosting component
* */
@Output()
formDisabled = new EventEmitter<boolean>();
/**
* Emits when the creation and download of the export-file have succeeded
* - Emits an null/empty string when exporting from an individual vault
* - Emits the organizationId when exporting from an organizationl vault
* */
@Output()
onSuccessfulExport = new EventEmitter<string>();
@Output() onSaved = new EventEmitter(); @Output() onSaved = new EventEmitter();
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
@@ -74,6 +135,11 @@ export class ExportComponent implements OnInit, OnDestroy {
) {} ) {}
async ngOnInit() { async ngOnInit() {
// Setup subscription to emit when this form is enabled/disabled
this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
this.formDisabled.emit(c === "DISABLED");
});
this.policyService this.policyService
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport) .policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@@ -88,8 +154,7 @@ export class ExportComponent implements OnInit, OnDestroy {
this.exportForm.get("format").valueChanges, this.exportForm.get("format").valueChanges,
this.exportForm.get("fileEncryptionType").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges,
) )
.pipe(takeUntil(this.destroy$)) .pipe(startWith(0), takeUntil(this.destroy$))
.pipe(startWith(0))
.subscribe(() => this.adjustValidators()); .subscribe(() => this.adjustValidators());
if (this.organizationId) { if (this.organizationId) {
@@ -118,6 +183,12 @@ export class ExportComponent implements OnInit, OnDestroy {
this.exportForm.controls.vaultSelector.setValue("myVault"); this.exportForm.controls.vaultSelector.setValue("myVault");
} }
ngAfterViewInit(): void {
this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
this.formLoading.emit(loading);
});
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
} }
@@ -187,6 +258,7 @@ export class ExportComponent implements OnInit, OnDestroy {
protected saved() { protected saved() {
this.onSaved.emit(); this.onSaved.emit();
this.onSuccessfulExport.emit(this.organizationId);
} }
private async verifyUser(): Promise<boolean> { private async verifyUser(): Promise<boolean> {
@@ -235,6 +307,10 @@ export class ExportComponent implements OnInit, OnDestroy {
} }
protected getFileName(prefix?: string) { protected getFileName(prefix?: string) {
if (this.organizationId) {
prefix = "org";
}
let extension = this.format; let extension = this.format;
if (this.format === "encrypted_json") { if (this.format === "encrypted_json") {
if (prefix == null) { if (prefix == null) {
@@ -248,7 +324,15 @@ export class ExportComponent implements OnInit, OnDestroy {
} }
protected async collectEvent(): Promise<void> { protected async collectEvent(): Promise<void> {
await this.eventCollectionService.collect(EventType.User_ClientExportedVault); if (this.organizationId) {
return await this.eventCollectionService.collect(
EventType.Organization_ClientExportedVault,
null,
false,
this.organizationId,
);
}
return await this.eventCollectionService.collect(EventType.User_ClientExportedVault);
} }
get format() { get format() {

17
libs/tools/send/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Bitwarden Send
This folder contains 2 packages that can be used to create and modify Sends.
## semd-core
Package name: `@bitwarden/send-core`
Contains all types, models, and services for Bitwarden Send
Currently in use by the Bitwarden Web Vault, CLI, desktop app and browser extension
## send-ui
Package name: `@bitwarden/send-ui`
Contains all UI components used for Bitwarden Send

View File

@@ -0,0 +1,13 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("../../../shared/tsconfig.libs");
/** @type {import('jest').Config} */
module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"],
preset: "ts-jest",
testEnvironment: "jsdom",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/../../../",
}),
};

View File

@@ -0,0 +1,23 @@
{
"name": "@bitwarden/send-ui",
"version": "0.0.0",
"description": "Angular components for Bitwarden Send",
"keywords": [
"bitwarden"
],
"author": "Bitwarden Inc.",
"homepage": "https://bitwarden.com",
"repository": {
"type": "git",
"url": "https://github.com/bitwarden/clients"
},
"license": "GPL-3.0",
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc",
"build:watch": "npm run clean && tsc -watch"
},
"dependencies": {
"@bitwarden/common": "file:../../../common"
}
}

View File

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../shared/tsconfig.libs",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}

2
package-lock.json generated
View File

@@ -193,7 +193,7 @@
}, },
"apps/browser": { "apps/browser": {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.5.0" "version": "2024.5.1"
}, },
"apps/cli": { "apps/cli": {
"name": "@bitwarden/cli", "name": "@bitwarden/cli",

View File

@@ -25,6 +25,7 @@
"@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/core": ["./libs/importer/src"],
"@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/send-ui": [".libs/tools/send/send-ui/src"],
"@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/vault": ["./libs/vault/src"], "@bitwarden/vault": ["./libs/vault/src"],

View File

@@ -27,6 +27,7 @@
"@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/core": ["./libs/importer/src"],
"@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/platform": ["./libs/platform/src"],
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
"@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/node/*": ["./libs/node/src/*"],
"@bitwarden/web-vault/*": ["./apps/web/src/*"], "@bitwarden/web-vault/*": ["./apps/web/src/*"],
"@bitwarden/vault": ["./libs/vault/src"], "@bitwarden/vault": ["./libs/vault/src"],