1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

Streamlines change pass notification logic. (#16435)

* Streamlines change pass notification logic.

* Includes test cases for all change password behavior.

* Allows multiple cipher views to be passed to notification.

* Removes assumptions about matching passwords.

* Includes current password match for now.

* [WIP] Fixes exact login match ignore for change. Partially updates save/update methods for ciphers.

* Removes password matching.

* Preserves nullable cipherId, set only while cipher action handled

* Updates comment.
This commit is contained in:
Miles Blackwood
2025-10-06 18:22:07 -04:00
committed by GitHub
parent aa3be491d7
commit 24e159250b
8 changed files with 276 additions and 135 deletions

View File

@@ -35,7 +35,7 @@ interface NotificationQueueMessage {
} }
type ChangePasswordNotificationData = { type ChangePasswordNotificationData = {
cipherId: CipherView["id"]; cipherIds: CipherView["id"][];
newPassword: string; newPassword: string;
}; };

View File

@@ -289,7 +289,6 @@ describe("NotificationBackground", () => {
let tab: chrome.tabs.Tab; let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender; let sender: chrome.runtime.MessageSender;
let getEnableAddedLoginPromptSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let pushAddLoginToQueueSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance;
@@ -306,10 +305,7 @@ describe("NotificationBackground", () => {
notificationBackground as any, notificationBackground as any,
"getEnableAddedLoginPrompt", "getEnableAddedLoginPrompt",
); );
getEnableChangedPasswordPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableChangedPasswordPrompt",
);
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue"); pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
pushChangePasswordToQueueSpy = jest.spyOn( pushChangePasswordToQueueSpy = jest.spyOn(
notificationBackground as any, notificationBackground as any,
@@ -368,24 +364,6 @@ describe("NotificationBackground", () => {
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
}); });
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]);
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to change the password for an existing login if the password has not changed", async () => { it("skips attempting to change the password for an existing login if the password has not changed", async () => {
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData; const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
@@ -445,37 +423,12 @@ describe("NotificationBackground", () => {
sender.tab, sender.tab,
); );
}); });
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
username: "tEsT",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id",
login: { username: "test", password: "oldPassword" },
}),
]);
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
data.password,
sender.tab,
);
});
}); });
describe("bgTriggerChangedPasswordNotification message handler", () => { describe("bgTriggerChangedPasswordNotification message handler", () => {
let tab: chrome.tabs.Tab; let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender; let sender: chrome.runtime.MessageSender;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance;
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = { const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
@@ -488,6 +441,11 @@ describe("NotificationBackground", () => {
beforeEach(() => { beforeEach(() => {
tab = createChromeTabMock(); tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab }); sender = mock<chrome.runtime.MessageSender>({ tab });
getEnableChangedPasswordPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableChangedPasswordPrompt",
);
pushChangePasswordToQueueSpy = jest.spyOn( pushChangePasswordToQueueSpy = jest.spyOn(
notificationBackground as any, notificationBackground as any,
"pushChangePasswordToQueue", "pushChangePasswordToQueue",
@@ -495,6 +453,40 @@ describe("NotificationBackground", () => {
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl"); getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
}); });
afterEach(() => {
getEnableChangedPasswordPromptSpy.mockRestore();
pushChangePasswordToQueueSpy.mockRestore();
getAllDecryptedForUrlSpy.mockRestore();
});
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the change password message to the queue if the user is logged out", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => { it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData; const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
@@ -503,7 +495,92 @@ describe("NotificationBackground", () => {
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
}); });
it("adds a change password message to the queue if the user does not have an unlocked account", async () => { it("only only includes ciphers in notification data matching a username if username was present in the modify form data", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
username: "userName",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id-1",
login: { username: "test", password: "currentPassword" },
}),
mock<CipherView>({
id: "cipher-id-2",
login: { username: "username", password: "currentPassword" },
}),
mock<CipherView>({
id: "cipher-id-3",
login: { username: "uSeRnAmE", password: "currentPassword" },
}),
]);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
["cipher-id-2", "cipher-id-3"],
"example.com",
data?.newPassword,
sender.tab,
);
});
it("adds a change password message to the queue with current password, if there is a current password, but no new password", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
password: "newPasswordUpdatedElsewhere",
newPassword: null,
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id-1",
login: { password: "currentPassword" },
}),
]);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
["cipher-id-1"],
"example.com",
data?.password,
sender.tab,
);
});
it("adds a change password message to the queue with new password, if new password is provided", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
password: "password2",
newPassword: "password3",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id-1",
login: { password: "password1" },
}),
mock<CipherView>({
id: "cipher-id-4",
login: { password: "password4" },
}),
]);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
["cipher-id-1", "cipher-id-4"],
"example.com",
data?.newPassword,
sender.tab,
);
});
it("adds a change password message to the queue if the user has a locked account", async () => {
const data: ModifyLoginCipherFormData = { const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData, ...mockModifyLoginCipherFormData,
uri: "https://example.com", uri: "https://example.com",
@@ -522,10 +599,12 @@ describe("NotificationBackground", () => {
); );
}); });
it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => { it("doesn't add a password if there is no current or new password", async () => {
const data: ModifyLoginCipherFormData = { const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData, ...mockModifyLoginCipherFormData,
uri: "https://example.com", uri: "https://example.com",
password: null,
newPassword: null,
}; };
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
@@ -537,23 +616,6 @@ describe("NotificationBackground", () => {
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
}); });
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
mock<CipherView>({ login: { username: "test2", password: "password" } }),
]);
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => { it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
const data: ModifyLoginCipherFormData = { const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData, ...mockModifyLoginCipherFormData,
@@ -570,28 +632,39 @@ describe("NotificationBackground", () => {
await notificationBackground.triggerChangedPasswordNotification(data, tab); await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id", ["cipher-id"],
"example.com", "example.com",
data?.newPassword, data?.newPassword,
sender.tab, sender.tab,
); );
}); });
it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => { it("adds a change password message with all matching ciphers if no current password is passed and more than one cipher is found for a url", async () => {
const data: ModifyLoginCipherFormData = { const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData, ...mockModifyLoginCipherFormData,
uri: "https://example.com", uri: "https://example.com",
password: null,
}; };
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }), mock<CipherView>({
mock<CipherView>({ login: { username: "test2", password: "password" } }), id: "cipher-id-1",
login: { username: "test", password: "password" },
}),
mock<CipherView>({
id: "cipher-id-2",
login: { username: "test2", password: "password" },
}),
]); ]);
await notificationBackground.triggerChangedPasswordNotification(data, tab); await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); ["cipher-id-1", "cipher-id-2"],
"example.com",
data?.newPassword,
sender.tab,
);
}); });
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => { it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
@@ -611,7 +684,7 @@ describe("NotificationBackground", () => {
await notificationBackground.triggerChangedPasswordNotification(data, tab); await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id", ["cipher-id"],
"example.com", "example.com",
data?.newPassword, data?.newPassword,
sender.tab, sender.tab,

View File

@@ -213,14 +213,26 @@ export default class NotificationBackground {
let cipherView: CipherView; let cipherView: CipherView;
if (cipherQueueMessage.type === NotificationType.ChangePassword) { if (cipherQueueMessage.type === NotificationType.ChangePassword) {
const { const {
data: { cipherId }, data: { cipherIds },
} = cipherQueueMessage; } = cipherQueueMessage;
cipherView = await this.getDecryptedCipherById(cipherId, activeUserId); const cipherViews = await this.cipherService.getAllDecrypted(activeUserId);
return cipherViews
.filter((cipher) => cipherIds.includes(cipher.id))
.map((cipherView) => {
const organizationType = getOrganizationType(cipherView.organizationId);
return this.convertToNotificationCipherData(
cipherView,
iconsServerUrl,
showFavicons,
organizationType,
);
});
} else { } else {
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage); cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
} }
const organizationType = getOrganizationType(cipherView.organizationId); const organizationType = getOrganizationType(cipherView.organizationId);
return [ return [
this.convertToNotificationCipherData( this.convertToNotificationCipherData(
cipherView, cipherView,
@@ -555,16 +567,6 @@ export default class NotificationBackground {
return true; return true;
} }
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
if (
changePasswordIsEnabled &&
usernameMatches.length === 1 &&
usernameMatches[0].login.password !== login.password
) {
await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab);
return true;
}
return false; return false;
} }
@@ -603,45 +605,92 @@ export default class NotificationBackground {
data: ModifyLoginCipherFormData, data: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
): Promise<boolean> { ): Promise<boolean> {
const changeData = { const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
url: data.uri, if (!changePasswordIsEnabled) {
currentPassword: data.password,
newPassword: data.newPassword,
};
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
return false; return false;
} }
const authStatus = await this.getAuthStatus();
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { if (authStatus === AuthenticationStatus.LoggedOut) {
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true); return false;
return true;
} }
let id: string = null;
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId), this.accountService.activeAccount$.pipe(getOptionalUserId),
); );
if (activeUserId == null) { if (activeUserId === null) {
return false;
}
const loginDomain = Utils.getDomain(data.uri);
if (loginDomain === null) {
return false; return false;
} }
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId); const username: string | null = data.username || null;
if (changeData.currentPassword != null) { const currentPassword = data.password || null;
const passwordMatches = ciphers.filter( const newPassword = data.newPassword || null;
(c) => c.login.password === changeData.currentPassword,
); if (authStatus === AuthenticationStatus.Locked && newPassword !== null) {
if (passwordMatches.length === 1) { await this.pushChangePasswordToQueue(null, loginDomain, newPassword, tab, true);
id = passwordMatches[0].id;
}
} else if (ciphers.length === 1) {
id = ciphers[0].id;
}
if (id != null) {
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
return true; return true;
} }
let ciphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
data.uri,
activeUserId,
);
const normalizedUsername: string = username ? username.toLowerCase() : "";
const shouldMatchUsername = typeof username === "string" && username.length > 0;
if (shouldMatchUsername) {
// Presence of a username should filter ciphers further.
ciphers = ciphers.filter(
(cipher) =>
cipher.login.username !== null &&
cipher.login.username.toLowerCase() === normalizedUsername,
);
}
if (ciphers.length === 1) {
const [cipher] = ciphers;
if (
username !== null &&
newPassword === null &&
cipher.login.username === normalizedUsername &&
cipher.login.password === currentPassword
) {
// Assumed to be a login
return false;
}
}
if (currentPassword && !newPassword) {
// Only use current password for change if no new password present.
if (ciphers.length > 0) {
await this.pushChangePasswordToQueue(
ciphers.map((cipher) => cipher.id),
loginDomain,
currentPassword,
tab,
);
return true;
}
}
if (newPassword) {
// Otherwise include all known ciphers.
if (ciphers.length > 0) {
await this.pushChangePasswordToQueue(
ciphers.map((cipher) => cipher.id),
loginDomain,
newPassword,
tab,
);
return true;
}
}
return false; return false;
} }
@@ -666,7 +715,7 @@ export default class NotificationBackground {
} }
private async pushChangePasswordToQueue( private async pushChangePasswordToQueue(
cipherId: string, cipherIds: CipherView["id"][],
loginDomain: string, loginDomain: string,
newPassword: string, newPassword: string,
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
@@ -677,7 +726,7 @@ export default class NotificationBackground {
const launchTimestamp = new Date().getTime(); const launchTimestamp = new Date().getTime();
const message: AddChangePasswordNotificationQueueMessage = { const message: AddChangePasswordNotificationQueueMessage = {
type: NotificationType.ChangePassword, type: NotificationType.ChangePassword,
data: { cipherId: cipherId, newPassword: newPassword }, data: { cipherIds: cipherIds, newPassword: newPassword },
domain: loginDomain, domain: loginDomain,
tab: tab, tab: tab,
launchTimestamp, launchTimestamp,
@@ -716,12 +765,12 @@ export default class NotificationBackground {
return; return;
} }
await this.saveOrUpdateCredentials(sender.tab, message.edit, message.folder); await this.saveOrUpdateCredentials(sender.tab, message.cipherId, message.edit, message.folder);
} }
async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) { async handleCipherUpdateRepromptResponse(message: NotificationBackgroundExtensionMessage) {
if (message.success) { if (message.success) {
await this.saveOrUpdateCredentials(message.tab, false, undefined, true); await this.saveOrUpdateCredentials(message.tab, message.cipherId, false, undefined, true);
} else { } else {
await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", { await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", {
error: "Password reprompt failed", error: "Password reprompt failed",
@@ -740,6 +789,7 @@ export default class NotificationBackground {
*/ */
private async saveOrUpdateCredentials( private async saveOrUpdateCredentials(
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
cipherId: CipherView["id"],
edit: boolean, edit: boolean,
folderId?: string, folderId?: string,
skipReprompt: boolean = false, skipReprompt: boolean = false,
@@ -764,7 +814,7 @@ export default class NotificationBackground {
if (queueMessage.type === NotificationType.ChangePassword) { if (queueMessage.type === NotificationType.ChangePassword) {
const { const {
data: { cipherId, newPassword }, data: { newPassword },
} = queueMessage; } = queueMessage;
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId); const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);

View File

@@ -455,12 +455,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
notificationType: NotificationType, notificationType: NotificationType,
): boolean => { ): boolean => {
switch (notificationType) { switch (notificationType) {
case NotificationTypes.Change:
return modifyLoginData?.newPassword && !modifyLoginData.username;
case NotificationTypes.Add: case NotificationTypes.Add:
return ( return (
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword) modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
); );
case NotificationTypes.Change:
return !!(modifyLoginData.password || modifyLoginData.newPassword);
case NotificationTypes.AtRiskPassword: case NotificationTypes.AtRiskPassword:
return !modifyLoginData.newPassword; return !modifyLoginData.newPassword;
case NotificationTypes.Unlock: case NotificationTypes.Unlock:

View File

@@ -4,8 +4,10 @@ import { BadgeButton } from "../../../content/components/buttons/badge-button";
import { EditButton } from "../../../content/components/buttons/edit-button"; import { EditButton } from "../../../content/components/buttons/edit-button";
import { NotificationTypes } from "../../../notification/abstractions/notification-bar"; import { NotificationTypes } from "../../../notification/abstractions/notification-bar";
import { I18n } from "../common-types"; import { I18n } from "../common-types";
import { selectedCipher as selectedCipherSignal } from "../signals/selected-cipher";
export type CipherActionProps = { export type CipherActionProps = {
cipherId: string;
handleAction?: (e: Event) => void; handleAction?: (e: Event) => void;
i18n: I18n; i18n: I18n;
itemName: string; itemName: string;
@@ -15,6 +17,7 @@ export type CipherActionProps = {
}; };
export function CipherAction({ export function CipherAction({
cipherId,
handleAction = () => { handleAction = () => {
/* no-op */ /* no-op */
}, },
@@ -24,9 +27,17 @@ export function CipherAction({
theme, theme,
username, username,
}: CipherActionProps) { }: CipherActionProps) {
const selectCipherHandleAction = (e: Event) => {
selectedCipherSignal.set(cipherId);
try {
handleAction(e);
} finally {
selectedCipherSignal.set(null);
}
};
return notificationType === NotificationTypes.Change return notificationType === NotificationTypes.Change
? BadgeButton({ ? BadgeButton({
buttonAction: handleAction, buttonAction: selectCipherHandleAction,
buttonText: i18n.notificationUpdate, buttonText: i18n.notificationUpdate,
itemName, itemName,
theme, theme,

View File

@@ -40,6 +40,7 @@ export function CipherItem({
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
cipherActionButton = html`<div> cipherActionButton = html`<div>
${CipherAction({ ${CipherAction({
cipherId: cipher.id,
handleAction, handleAction,
i18n, i18n,
itemName: name, itemName: name,

View File

@@ -0,0 +1,3 @@
import { signal } from "@lit-labs/signals";
export const selectedCipher = signal<string | null>(null);

View File

@@ -1,6 +1,7 @@
import { render } from "lit"; import { render } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { NotificationCipherData } from "../content/components/cipher/types"; import { NotificationCipherData } from "../content/components/cipher/types";
@@ -8,6 +9,7 @@ import { CollectionView, I18n, OrgView } from "../content/components/common-type
import { AtRiskNotification } from "../content/components/notification/at-risk-password/container"; import { AtRiskNotification } from "../content/components/notification/at-risk-password/container";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
import { NotificationContainer } from "../content/components/notification/container"; import { NotificationContainer } from "../content/components/notification/container";
import { selectedCipher as selectedCipherSignal } from "../content/components/signals/selected-cipher";
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder"; import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault"; import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
@@ -180,9 +182,9 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
const i18n = getI18n(); const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
const resolvedType = resolveNotificationType(notificationBarIframeInitData); const notificationType = resolveNotificationType(notificationBarIframeInitData);
const headerMessage = getNotificationHeaderMessage(i18n, resolvedType); const headerMessage = getNotificationHeaderMessage(i18n, notificationType);
const notificationTestId = getNotificationTestId(resolvedType); const notificationTestId = getNotificationTestId(notificationType);
appendHeaderMessageToTitle(headerMessage); appendHeaderMessageToTitle(headerMessage);
document.body.innerHTML = ""; document.body.innerHTML = "";
@@ -191,7 +193,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
const notificationConfig = { const notificationConfig = {
...notificationBarIframeInitData, ...notificationBarIframeInitData,
headerMessage, headerMessage,
type: resolvedType, type: notificationType,
notificationTestId, notificationTestId,
theme: resolvedTheme, theme: resolvedTheme,
personalVaultIsAllowed: !personalVaultDisallowed, personalVaultIsAllowed: !personalVaultDisallowed,
@@ -201,7 +203,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
}; };
const handleSaveAction = () => { const handleSaveAction = () => {
sendSaveCipherMessage(true); // cipher ID is null while vault is locked.
sendSaveCipherMessage(null, true);
render( render(
NotificationContainer({ NotificationContainer({
@@ -262,7 +265,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
NotificationContainer({ NotificationContainer({
...notificationBarIframeInitData, ...notificationBarIframeInitData,
headerMessage, headerMessage,
type: resolvedType, type: notificationType,
theme: resolvedTheme, theme: resolvedTheme,
notificationTestId, notificationTestId,
personalVaultIsAllowed: !personalVaultDisallowed, personalVaultIsAllowed: !personalVaultDisallowed,
@@ -276,9 +279,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
}); });
function handleEditOrUpdateAction(e: Event) { function handleEditOrUpdateAction(e: Event) {
const notificationType = initData?.type;
e.preventDefault(); e.preventDefault();
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false); sendSaveCipherMessage(selectedCipherSignal.get(), notificationType === NotificationTypes.Add);
} }
} }
@@ -291,6 +293,7 @@ function handleCloseNotification(e: Event) {
} }
function handleSaveAction(e: Event) { function handleSaveAction(e: Event) {
const selectedCipher = selectedCipherSignal.get();
const selectedVault = selectedVaultSignal.get(); const selectedVault = selectedVaultSignal.get();
const selectedFolder = selectedFolderSignal.get(); const selectedFolder = selectedFolderSignal.get();
@@ -304,16 +307,16 @@ function handleSaveAction(e: Event) {
} }
e.preventDefault(); e.preventDefault();
sendSaveCipherMessage(selectedCipher, removeIndividualVault(), selectedFolder);
sendSaveCipherMessage(removeIndividualVault(), selectedFolder);
if (removeIndividualVault()) { if (removeIndividualVault()) {
return; return;
} }
} }
function sendSaveCipherMessage(edit: boolean, folder?: string) { function sendSaveCipherMessage(cipherId: CipherView["id"] | null, edit: boolean, folder?: string) {
sendPlatformMessage({ sendPlatformMessage({
command: "bgSaveCipher", command: "bgSaveCipher",
cipherId,
folder, folder,
edit, edit,
}); });