mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +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:
@@ -35,7 +35,7 @@ interface NotificationQueueMessage {
|
||||
}
|
||||
|
||||
type ChangePasswordNotificationData = {
|
||||
cipherId: CipherView["id"];
|
||||
cipherIds: CipherView["id"][];
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -289,7 +289,6 @@ describe("NotificationBackground", () => {
|
||||
let tab: chrome.tabs.Tab;
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
@@ -306,10 +305,7 @@ describe("NotificationBackground", () => {
|
||||
notificationBackground as any,
|
||||
"getEnableAddedLoginPrompt",
|
||||
);
|
||||
getEnableChangedPasswordPromptSpy = jest.spyOn(
|
||||
notificationBackground as any,
|
||||
"getEnableChangedPasswordPrompt",
|
||||
);
|
||||
|
||||
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
|
||||
pushChangePasswordToQueueSpy = jest.spyOn(
|
||||
notificationBackground as any,
|
||||
@@ -368,24 +364,6 @@ describe("NotificationBackground", () => {
|
||||
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 () => {
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
@@ -445,37 +423,12 @@ describe("NotificationBackground", () => {
|
||||
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", () => {
|
||||
let tab: chrome.tabs.Tab;
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||
@@ -488,6 +441,11 @@ describe("NotificationBackground", () => {
|
||||
beforeEach(() => {
|
||||
tab = createChromeTabMock();
|
||||
sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
getEnableChangedPasswordPromptSpy = jest.spyOn(
|
||||
notificationBackground as any,
|
||||
"getEnableChangedPasswordPrompt",
|
||||
);
|
||||
|
||||
pushChangePasswordToQueueSpy = jest.spyOn(
|
||||
notificationBackground as any,
|
||||
"pushChangePasswordToQueue",
|
||||
@@ -495,6 +453,40 @@ describe("NotificationBackground", () => {
|
||||
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 () => {
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
|
||||
@@ -503,7 +495,92 @@ describe("NotificationBackground", () => {
|
||||
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 = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
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 = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
password: null,
|
||||
newPassword: null,
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@@ -537,23 +616,6 @@ describe("NotificationBackground", () => {
|
||||
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 () => {
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
@@ -570,28 +632,39 @@ describe("NotificationBackground", () => {
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
["cipher-id"],
|
||||
"example.com",
|
||||
data?.newPassword,
|
||||
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 = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
password: null,
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
||||
mock<CipherView>({
|
||||
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);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
["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 () => {
|
||||
@@ -611,7 +684,7 @@ describe("NotificationBackground", () => {
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
["cipher-id"],
|
||||
"example.com",
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
|
||||
@@ -213,14 +213,26 @@ export default class NotificationBackground {
|
||||
let cipherView: CipherView;
|
||||
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId },
|
||||
data: { cipherIds },
|
||||
} = 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 {
|
||||
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
}
|
||||
|
||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||
|
||||
return [
|
||||
this.convertToNotificationCipherData(
|
||||
cipherView,
|
||||
@@ -555,16 +567,6 @@ export default class NotificationBackground {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -603,45 +605,92 @@ export default class NotificationBackground {
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const changeData = {
|
||||
url: data.uri,
|
||||
currentPassword: data.password,
|
||||
newPassword: data.newPassword,
|
||||
};
|
||||
|
||||
const loginDomain = Utils.getDomain(changeData.url);
|
||||
if (loginDomain == null) {
|
||||
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
||||
if (!changePasswordIsEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
|
||||
return true;
|
||||
const authStatus = await this.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let id: string = null;
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
if (activeUserId == null) {
|
||||
if (activeUserId === null) {
|
||||
return false;
|
||||
}
|
||||
const loginDomain = Utils.getDomain(data.uri);
|
||||
if (loginDomain === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
|
||||
if (changeData.currentPassword != null) {
|
||||
const passwordMatches = ciphers.filter(
|
||||
(c) => c.login.password === changeData.currentPassword,
|
||||
);
|
||||
if (passwordMatches.length === 1) {
|
||||
id = passwordMatches[0].id;
|
||||
}
|
||||
} else if (ciphers.length === 1) {
|
||||
id = ciphers[0].id;
|
||||
}
|
||||
if (id != null) {
|
||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
|
||||
const username: string | null = data.username || null;
|
||||
const currentPassword = data.password || null;
|
||||
const newPassword = data.newPassword || null;
|
||||
|
||||
if (authStatus === AuthenticationStatus.Locked && newPassword !== null) {
|
||||
await this.pushChangePasswordToQueue(null, loginDomain, newPassword, tab, 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;
|
||||
}
|
||||
|
||||
@@ -666,7 +715,7 @@ export default class NotificationBackground {
|
||||
}
|
||||
|
||||
private async pushChangePasswordToQueue(
|
||||
cipherId: string,
|
||||
cipherIds: CipherView["id"][],
|
||||
loginDomain: string,
|
||||
newPassword: string,
|
||||
tab: chrome.tabs.Tab,
|
||||
@@ -677,7 +726,7 @@ export default class NotificationBackground {
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddChangePasswordNotificationQueueMessage = {
|
||||
type: NotificationType.ChangePassword,
|
||||
data: { cipherId: cipherId, newPassword: newPassword },
|
||||
data: { cipherIds: cipherIds, newPassword: newPassword },
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -716,12 +765,12 @@ export default class NotificationBackground {
|
||||
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) {
|
||||
if (message.success) {
|
||||
await this.saveOrUpdateCredentials(message.tab, false, undefined, true);
|
||||
await this.saveOrUpdateCredentials(message.tab, message.cipherId, false, undefined, true);
|
||||
} else {
|
||||
await BrowserApi.tabSendMessageData(message.tab, "saveCipherAttemptCompleted", {
|
||||
error: "Password reprompt failed",
|
||||
@@ -740,6 +789,7 @@ export default class NotificationBackground {
|
||||
*/
|
||||
private async saveOrUpdateCredentials(
|
||||
tab: chrome.tabs.Tab,
|
||||
cipherId: CipherView["id"],
|
||||
edit: boolean,
|
||||
folderId?: string,
|
||||
skipReprompt: boolean = false,
|
||||
@@ -764,7 +814,7 @@ export default class NotificationBackground {
|
||||
|
||||
if (queueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId, newPassword },
|
||||
data: { newPassword },
|
||||
} = queueMessage;
|
||||
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
|
||||
|
||||
@@ -455,12 +455,12 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
notificationType: NotificationType,
|
||||
): boolean => {
|
||||
switch (notificationType) {
|
||||
case NotificationTypes.Change:
|
||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||
case NotificationTypes.Add:
|
||||
return (
|
||||
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
||||
);
|
||||
case NotificationTypes.Change:
|
||||
return !!(modifyLoginData.password || modifyLoginData.newPassword);
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
return !modifyLoginData.newPassword;
|
||||
case NotificationTypes.Unlock:
|
||||
|
||||
@@ -4,8 +4,10 @@ import { BadgeButton } from "../../../content/components/buttons/badge-button";
|
||||
import { EditButton } from "../../../content/components/buttons/edit-button";
|
||||
import { NotificationTypes } from "../../../notification/abstractions/notification-bar";
|
||||
import { I18n } from "../common-types";
|
||||
import { selectedCipher as selectedCipherSignal } from "../signals/selected-cipher";
|
||||
|
||||
export type CipherActionProps = {
|
||||
cipherId: string;
|
||||
handleAction?: (e: Event) => void;
|
||||
i18n: I18n;
|
||||
itemName: string;
|
||||
@@ -15,6 +17,7 @@ export type CipherActionProps = {
|
||||
};
|
||||
|
||||
export function CipherAction({
|
||||
cipherId,
|
||||
handleAction = () => {
|
||||
/* no-op */
|
||||
},
|
||||
@@ -24,9 +27,17 @@ export function CipherAction({
|
||||
theme,
|
||||
username,
|
||||
}: CipherActionProps) {
|
||||
const selectCipherHandleAction = (e: Event) => {
|
||||
selectedCipherSignal.set(cipherId);
|
||||
try {
|
||||
handleAction(e);
|
||||
} finally {
|
||||
selectedCipherSignal.set(null);
|
||||
}
|
||||
};
|
||||
return notificationType === NotificationTypes.Change
|
||||
? BadgeButton({
|
||||
buttonAction: handleAction,
|
||||
buttonAction: selectCipherHandleAction,
|
||||
buttonText: i18n.notificationUpdate,
|
||||
itemName,
|
||||
theme,
|
||||
|
||||
@@ -40,6 +40,7 @@ export function CipherItem({
|
||||
if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) {
|
||||
cipherActionButton = html`<div>
|
||||
${CipherAction({
|
||||
cipherId: cipher.id,
|
||||
handleAction,
|
||||
i18n,
|
||||
itemName: name,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { signal } from "@lit-labs/signals";
|
||||
|
||||
export const selectedCipher = signal<string | null>(null);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render } from "lit";
|
||||
|
||||
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 { 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 { NotificationConfirmationContainer } from "../content/components/notification/confirmation/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 { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault";
|
||||
|
||||
@@ -180,9 +182,9 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const i18n = getI18n();
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
|
||||
const resolvedType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getNotificationHeaderMessage(i18n, resolvedType);
|
||||
const notificationTestId = getNotificationTestId(resolvedType);
|
||||
const notificationType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getNotificationHeaderMessage(i18n, notificationType);
|
||||
const notificationTestId = getNotificationTestId(notificationType);
|
||||
appendHeaderMessageToTitle(headerMessage);
|
||||
|
||||
document.body.innerHTML = "";
|
||||
@@ -191,7 +193,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const notificationConfig = {
|
||||
...notificationBarIframeInitData,
|
||||
headerMessage,
|
||||
type: resolvedType,
|
||||
type: notificationType,
|
||||
notificationTestId,
|
||||
theme: resolvedTheme,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
@@ -201,7 +203,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
};
|
||||
|
||||
const handleSaveAction = () => {
|
||||
sendSaveCipherMessage(true);
|
||||
// cipher ID is null while vault is locked.
|
||||
sendSaveCipherMessage(null, true);
|
||||
|
||||
render(
|
||||
NotificationContainer({
|
||||
@@ -262,7 +265,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
NotificationContainer({
|
||||
...notificationBarIframeInitData,
|
||||
headerMessage,
|
||||
type: resolvedType,
|
||||
type: notificationType,
|
||||
theme: resolvedTheme,
|
||||
notificationTestId,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
@@ -276,9 +279,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
});
|
||||
|
||||
function handleEditOrUpdateAction(e: Event) {
|
||||
const notificationType = initData?.type;
|
||||
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) {
|
||||
const selectedCipher = selectedCipherSignal.get();
|
||||
const selectedVault = selectedVaultSignal.get();
|
||||
const selectedFolder = selectedFolderSignal.get();
|
||||
|
||||
@@ -304,16 +307,16 @@ function handleSaveAction(e: Event) {
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
sendSaveCipherMessage(removeIndividualVault(), selectedFolder);
|
||||
sendSaveCipherMessage(selectedCipher, removeIndividualVault(), selectedFolder);
|
||||
if (removeIndividualVault()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function sendSaveCipherMessage(edit: boolean, folder?: string) {
|
||||
function sendSaveCipherMessage(cipherId: CipherView["id"] | null, edit: boolean, folder?: string) {
|
||||
sendPlatformMessage({
|
||||
command: "bgSaveCipher",
|
||||
cipherId,
|
||||
folder,
|
||||
edit,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user