From 3367339f9ba73ab6718ca949750e685e21738bf9 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Fri, 23 Feb 2024 10:50:11 -0600 Subject: [PATCH] [PM-2753] Prompt to Save New Login Credentials Silently Drops Data on Network Error (#7730) * [PM-673] Safari Notification Bar Does Not Show Folders * [PM-673] Refactoring Context Menu Implementations to Ensure Pages with No Logins Can Dismiss Notification Bar * [PM-673] Refactoring typing information for the LockedVaultPendingNotificationsItem typing * [PM-673] Refactoring typing information for notification background * [PM-673] Finishing out typing for potential extension messages to the notification bar; * [PM-673] Working through implementation details for the notification background * [PM-673] Fixing issues present with messaging re-implementation * [PM-673] Fixing issue with folders not populating within Safari notification bar * [PM-673] Fixing jest test issues present within implementation * [PM-673] Fixing issue present with webVaultUrl vulnerability * [PM-673] Fixing XSS Vulnerability within Notification Bar; * [PM-5670] Putting together a partial implementation for having messages appear on network error within the notification bar * [PM-673] Incorporating status update for when user has successfully saved credentials * [PM-673] Incorporating status update for when user has successfully saved credentials * [PM-5949] Refactor typing information for notification bar * [PM-5949] Fix jest tests for overlay background * [PM-5949] Removing unnused typing data * [PM-5949] Fixing lint error * [PM-5949] Adding jest tests for convertAddLoginQueueMessageToCipherView method * [PM-5949] Fixing jest test for overlay * [PM-5950] Fix Context Menu Update Race Condition and Refactor Implementation * [PM-5950] Adding jest test for cipherContextMenu.update method * [PM-5950] Adding documentation for method within MainContextMenuHandler * [PM-5950] Adding jest tests for the mainContextMenuHandler * [PM-673] Stripping unnecessary work for network drop issue * [PM-673] Stripping unnecessary work for network drop issue * [PM-2753] Prompt to Save New Login Credentials Silently Drops Data on Network Error * [PM-673] Stripping out work done for another ticket * [PM-5950] Removing unnecessary return value from MainContextMenuHandler.create method * [PM-673] Implementing unit test coverage for newly introduced logic * [PM-673] Implementing unit test coverage for newly introduced logic * [PM-673] Implementing unit test coverage for newly introduced logic * [PM-673] Implementing unit test coverage for newly introduced logic * [PM-2753] Implementing jest tests to validate logic changes * [PM-2753] Implementing jest tests to validate logic changes * [PM-2753] Implementing jest tests to validate logic changes * [PM-2753] Implementing jest tests to validate logic changes * [PM-2753] Incorporating addition of green and red borders when success or error events occur * [PM-5950] Fixing unawaited context menu promise * [PM-673] Merging changes in from main and fixing merge conflicts * [PM-2753] Merging work in from main and resolving merge conflicts * [PM-673] Fixing issue where updates to the added login were not triggering correctly * [PM-673] Merging changes in from main and fixing merge conflicts --- apps/browser/src/_locales/en/messages.json | 16 +- .../abstractions/notification.background.ts | 3 + .../notification.background.spec.ts | 419 ++++++++++++++++++ .../background/notification.background.ts | 102 +++-- .../src/autofill/content/notification-bar.ts | 12 + .../abstractions/notification-bar.ts | 13 + .../src/autofill/notification/bar.html | 6 +- .../src/autofill/notification/bar.scss | 12 + apps/browser/src/autofill/notification/bar.ts | 78 +++- 9 files changed, 618 insertions(+), 43 deletions(-) create mode 100644 apps/browser/src/autofill/notification/abstractions/notification-bar.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f8df49c797..cf0ce67980 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2542,8 +2542,8 @@ "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." + "message": "Import to Bitwarden", + "description": "Notification button text for starting a fileless import." }, "importing": { "message": "Importing...", @@ -2993,5 +2993,17 @@ "makeDefault": { "message": "Make default", "description": "Button text for the setting that allows overriding the default browser autofill settings" + }, + "saveCipherAttemptSuccess": { + "message": "Credentials saved successfully!", + "description": "Notification message for when saving credentials has succeeded." + }, + "updateCipherAttemptSuccess": { + "message": "Credentials updated successfully!", + "description": "Notification message for when updating credentials has succeeded." + }, + "saveCipherAttemptFailed": { + "message": "Error saving credentials. Check console for details.", + "description": "Notification message for when saving credentials has failed." } } diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 8b7cbf5059..ba6d18edbc 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -88,6 +88,8 @@ type NotificationBackgroundExtensionMessage = { notificationType?: string; }; +type SaveOrUpdateCipherResult = undefined | { error: string }; + type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage }; type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; @@ -120,6 +122,7 @@ export { ChangePasswordMessageData, UnlockVaultMessageData, AddLoginMessageData, + SaveOrUpdateCipherResult, NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 5bee7d4093..af92dae9eb 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -20,6 +20,7 @@ import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/auto import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils"; import { + AddChangePasswordQueueMessage, AddLoginQueueMessage, AddUnlockVaultQueueMessage, LockedVaultPendingNotificationsData, @@ -640,6 +641,424 @@ describe("NotificationBackground", () => { }); }); + describe("bgSaveCipher message handler", () => { + let getAuthStatusSpy: jest.SpyInstance; + let tabSendMessageDataSpy: jest.SpyInstance; + let openUnlockPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); + tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + openUnlockPopoutSpy = jest + .spyOn(notificationBackground as any, "openUnlockPopout") + .mockImplementation(); + }); + + it("skips saving the cipher and opens an unlock popout if the extension is not unlocked", async () => { + const sender = mock({ tab: { id: 1 } }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { message, sender }, + target: "notification.background", + }, + ); + expect(openUnlockPopoutSpy).toHaveBeenCalledWith(sender.tab); + }); + + describe("saveOrUpdateCredentials", () => { + let getDecryptedCipherByIdSpy: jest.SpyInstance; + let getAllDecryptedForUrlSpy: jest.SpyInstance; + let updatePasswordSpy: jest.SpyInstance; + let convertAddLoginQueueMessageToCipherViewSpy: jest.SpyInstance; + let tabSendMessageSpy: jest.SpyInstance; + let editItemSpy: jest.SpyInstance; + let setAddEditCipherInfoSpy: jest.SpyInstance; + let openAddEditVaultItemPopoutSpy: jest.SpyInstance; + let createWithServerSpy: jest.SpyInstance; + let updateWithServerSpy: jest.SpyInstance; + let folderExistsSpy: jest.SpyInstance; + let cipherEncryptSpy: jest.SpyInstance; + + beforeEach(() => { + getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + getDecryptedCipherByIdSpy = jest.spyOn( + notificationBackground as any, + "getDecryptedCipherById", + ); + getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl"); + updatePasswordSpy = jest.spyOn(notificationBackground as any, "updatePassword"); + convertAddLoginQueueMessageToCipherViewSpy = jest.spyOn( + notificationBackground as any, + "convertAddLoginQueueMessageToCipherView", + ); + tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation(); + editItemSpy = jest.spyOn(notificationBackground as any, "editItem"); + setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo"); + openAddEditVaultItemPopoutSpy = jest.spyOn( + notificationBackground as any, + "openAddEditVaultItemPopout", + ); + createWithServerSpy = jest.spyOn(cipherService, "createWithServer"); + updateWithServerSpy = jest.spyOn(cipherService, "updateWithServer"); + folderExistsSpy = jest.spyOn(notificationBackground as any, "folderExists"); + cipherEncryptSpy = jest.spyOn(cipherService, "encrypt"); + }); + + it("skips saving the cipher if the notification queue does not have a tab that is related to the sender", async () => { + const sender = mock({ tab: { id: 2 } }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + notificationBackground["notificationQueue"] = [ + mock({ + tab: createChromeTabMock({ id: 1 }), + }), + ]; + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updatePasswordSpy).not.toHaveBeenCalled(); + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + }); + + it("skips saving the cipher if the notification queue does not contain an AddLogin or ChangePassword type", async () => { + const tab = createChromeTabMock({ id: 1 }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + notificationBackground["notificationQueue"] = [ + mock({ + tab, + type: NotificationQueueMessageType.UnlockVault, + }), + ]; + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updatePasswordSpy).not.toHaveBeenCalled(); + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + }); + + it("skips saving the cipher if the notification queue message has a different domain than the passed tab", () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + notificationBackground["notificationQueue"] = [ + mock({ + type: NotificationQueueMessageType.AddLogin, + tab, + domain: "another.com", + }), + ]; + + sendExtensionRuntimeMessage(message, sender); + expect(updatePasswordSpy).not.toHaveBeenCalled(); + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + }); + + it("updates the password if the notification message type is for ChangePassword", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock(); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.newPassword, + message.edit, + sender.tab, + ); + expect(updateWithServerSpy).toHaveBeenCalled(); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "saveCipherAttemptCompleted", + }); + }); + + it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.AddLogin, + tab, + domain: "example.com", + username: "test", + password: "updated-password", + wasVaultLocked: true, + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + login: { username: "test", password: "old-password" }, + }); + getAllDecryptedForUrlSpy.mockResolvedValueOnce([cipherView]); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.password, + message.edit, + sender.tab, + ); + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + }); + + it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to update an existing cipher", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: true, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock(); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + setAddEditCipherInfoSpy.mockResolvedValue(undefined); + openAddEditVaultItemPopoutSpy.mockResolvedValue(undefined); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.newPassword, + message.edit, + sender.tab, + ); + expect(editItemSpy).toHaveBeenCalled(); + expect(updateWithServerSpy).not.toHaveBeenCalled(); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "closeNotificationBar", + }); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "editedCipher", + }); + expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({ + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { + cipherId: cipherView.id, + }); + }); + + it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to save the cipher", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: true, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.AddLogin, + tab, + domain: "example.com", + username: "test", + password: "password", + wasVaultLocked: false, + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + login: { username: "test", password: "password" }, + }); + folderExistsSpy.mockResolvedValueOnce(true); + convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView); + editItemSpy.mockResolvedValueOnce(undefined); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updatePasswordSpy).not.toHaveBeenCalled(); + expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith( + queueMessage, + message.folder, + ); + expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "closeNotificationBar", + }); + expect(createWithServerSpy).not.toHaveBeenCalled(); + }); + + it("creates the cipher within the server and sends an `saveCipherAttemptCompleted` and `addedCipher` message to the sender tab", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.AddLogin, + tab, + domain: "example.com", + username: "test", + password: "password", + wasVaultLocked: false, + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + login: { username: "test", password: "password" }, + }); + folderExistsSpy.mockResolvedValueOnce(false); + convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView); + editItemSpy.mockResolvedValueOnce(undefined); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith( + queueMessage, + null, + ); + expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView); + expect(createWithServerSpy).toHaveBeenCalled(); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "saveCipherAttemptCompleted", + }); + expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" }); + }); + + it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be saved to the server", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.AddLogin, + tab, + domain: "example.com", + username: "test", + password: "password", + wasVaultLocked: false, + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + login: { username: "test", password: "password" }, + }); + folderExistsSpy.mockResolvedValueOnce(true); + convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView); + editItemSpy.mockResolvedValueOnce(undefined); + const errorMessage = "fetch error"; + createWithServerSpy.mockImplementation(() => { + throw new Error(errorMessage); + }); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView); + expect(createWithServerSpy).toThrow(errorMessage); + expect(tabSendMessageSpy).not.toHaveBeenCalledWith(sender.tab, { + command: "addedCipher", + }); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "saveCipherAttemptCompleted", + { + error: errorMessage, + }, + ); + }); + + it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be updated within the server", async () => { + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock(); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + const errorMessage = "fetch error"; + updateWithServerSpy.mockImplementation(() => { + throw new Error(errorMessage); + }); + + sendExtensionRuntimeMessage(message, sender); + await flushPromises(); + + expect(updateWithServerSpy).toThrow(errorMessage); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "saveCipherAttemptCompleted", + { + error: errorMessage, + }, + ); + }); + }); + }); + describe("bgNeverSave message handler", () => { let tabSendMessageDataSpy: jest.SpyInstance; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index cb49e47eb6..42d8d47b23 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -39,6 +39,7 @@ import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.backgr export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; + private openAddEditVaultItemPopout = openAddEditVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), @@ -431,6 +432,14 @@ export default class NotificationBackground { this.removeTabFromNotificationQueue(tab); } + /** + * Saves a cipher based on the message sent from the notification bar. If the vault + * is locked, the message will be added to the notification queue and the unlock + * popout will be opened. + * + * @param message - The extension message + * @param sender - The contextual sender of the message + */ private async handleSaveCipherMessage( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, @@ -454,6 +463,14 @@ export default class NotificationBackground { await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder); } + /** + * Saves or updates credentials based on the message within the + * notification queue that is associated with the specified tab. + * + * @param tab - The tab to save or update credentials for + * @param edit - Identifies if the credentials should be edited or simply added + * @param folderId - The folder to add the cipher to + */ private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, edit: boolean, folderId?: string) { for (let i = this.notificationQueue.length - 1; i >= 0; i--) { const queueMessage = this.notificationQueue[i]; @@ -471,9 +488,6 @@ export default class NotificationBackground { } this.notificationQueue.splice(i, 1); - BrowserApi.tabSendMessageData(tab, "closeNotificationBar").catch((error) => - this.logService.error(error), - ); if (queueMessage.type === NotificationQueueMessageType.ChangePassword) { const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId); @@ -481,38 +495,52 @@ export default class NotificationBackground { return; } - if (queueMessage.type === NotificationQueueMessageType.AddLogin) { - // If the vault was locked, check if a cipher needs updating instead of creating a new one - if (queueMessage.wasVaultLocked) { - const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri); - const existingCipher = allCiphers.find( - (c) => - c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, - ); + // If the vault was locked, check if a cipher needs updating instead of creating a new one + if (queueMessage.wasVaultLocked) { + const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri); + const existingCipher = allCiphers.find( + (c) => + c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, + ); - if (existingCipher != null) { - await this.updatePassword(existingCipher, queueMessage.password, edit, tab); - return; - } - } - - folderId = (await this.folderExists(folderId)) ? folderId : null; - const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId); - - if (edit) { - await this.editItem(newCipher, tab); + if (existingCipher != null) { + await this.updatePassword(existingCipher, queueMessage.password, edit, tab); return; } + } - const cipher = await this.cipherService.encrypt(newCipher); + folderId = (await this.folderExists(folderId)) ? folderId : null; + const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId); + + if (edit) { + await this.editItem(newCipher, tab); + await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); + return; + } + + const cipher = await this.cipherService.encrypt(newCipher); + try { await this.cipherService.createWithServer(cipher); - BrowserApi.tabSendMessageData(tab, "addedCipher").catch((error) => - this.logService.error(error), - ); + await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" }); + await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); + } catch (error) { + await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { + error: String(error.message), + }); } } } + /** + * Handles updating an existing cipher's password. If the cipher + * is being edited, a popup will be opened to allow the user to + * edit the cipher. + * + * @param cipherView - The cipher to update + * @param newPassword - The new password to update the cipher with + * @param edit - Identifies if the cipher should be edited or simply updated + * @param tab - The tab that the message was sent from + */ private async updatePassword( cipherView: CipherView, newPassword: string, @@ -523,23 +551,37 @@ export default class NotificationBackground { if (edit) { await this.editItem(cipherView, tab); + await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); await BrowserApi.tabSendMessage(tab, { command: "editedCipher" }); return; } const cipher = await this.cipherService.encrypt(cipherView); - await this.cipherService.updateWithServer(cipher); - // We've only updated the password, no need to broadcast editedCipher message - return; + try { + // We've only updated the password, no need to broadcast editedCipher message + await this.cipherService.updateWithServer(cipher); + await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" }); + } catch (error) { + await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { + error: String(error.message), + }); + } } + /** + * Sets the add/edit cipher info in the state service + * and opens the add/edit vault item popout. + * + * @param cipherView - The cipher to edit + * @param senderTab - The tab that the message was sent from + */ private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { await this.stateService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); - await openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); + await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); } private async folderExists(folderId: string) { diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 175f5ada2c..ac20401da5 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -194,6 +194,18 @@ async function loadNotificationBar() { watchForms(msg.data.forms); sendResponse(); return true; + } else if (msg.command === "saveCipherAttemptCompleted") { + if (!notificationBarIframe) { + return; + } + + notificationBarIframe.contentWindow?.postMessage( + { + command: "saveCipherAttemptCompleted", + error: msg.data?.error, + }, + "*", + ); } } // End Message Processing diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts new file mode 100644 index 0000000000..e2753893b1 --- /dev/null +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -0,0 +1,13 @@ +import { SaveOrUpdateCipherResult } from "../../background/abstractions/notification.background"; + +type NotificationBarWindowMessage = { + [key: string]: any; + command: string; +}; + +type NotificationBarWindowMessageHandlers = { + [key: string]: CallableFunction; + saveCipherAttemptCompleted: ({ message }: { message: SaveOrUpdateCipherResult }) => void; +}; + +export { NotificationBarWindowMessage, NotificationBarWindowMessageHandlers }; diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 057e4273bc..26d9d7086d 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -6,7 +6,7 @@ -