mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
PM-19741 Adds a notification at login for at-risk passwords. (#14555)
Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
This commit is contained in:
@@ -2515,6 +2515,10 @@
|
|||||||
"change": {
|
"change": {
|
||||||
"message": "Change"
|
"message": "Change"
|
||||||
},
|
},
|
||||||
|
"changePassword": {
|
||||||
|
"message": "Change password",
|
||||||
|
"description": "Change password button for browser at risk notification on login."
|
||||||
|
},
|
||||||
"changeButtonTitle": {
|
"changeButtonTitle": {
|
||||||
"message": "Change password - $ITEMNAME$",
|
"message": "Change password - $ITEMNAME$",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -2524,6 +2528,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"atRiskPassword": {
|
||||||
|
"message": "At-risk password"
|
||||||
|
},
|
||||||
"atRiskPasswords": {
|
"atRiskPasswords": {
|
||||||
"message": "At-risk passwords"
|
"message": "At-risk passwords"
|
||||||
},
|
},
|
||||||
@@ -2558,6 +2565,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"atRiskChangePrompt": {
|
||||||
|
"message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.",
|
||||||
|
"placeholders": {
|
||||||
|
"organization": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Acme Corp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Notification body when a login triggers an at-risk password change request and the change password domain is known."
|
||||||
|
},
|
||||||
|
"atRiskNavigatePrompt": {
|
||||||
|
"message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.",
|
||||||
|
"placeholders": {
|
||||||
|
"organization": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Acme Corp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Notification body when a login triggers an at-risk password change request and no change password domain is provided."
|
||||||
|
},
|
||||||
"reviewAndChangeAtRiskPassword": {
|
"reviewAndChangeAtRiskPassword": {
|
||||||
"message": "Review and change one at-risk password"
|
"message": "Review and change one at-risk password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||||
|
|
||||||
import { CollectionView } from "../../content/components/common-types";
|
import { CollectionView } from "../../content/components/common-types";
|
||||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||||
@@ -32,10 +35,17 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage {
|
|||||||
type: "unlock";
|
type: "unlock";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
|
||||||
|
type: "at-risk-password";
|
||||||
|
organizationName: string;
|
||||||
|
passwordChangeUri?: string;
|
||||||
|
}
|
||||||
|
|
||||||
type NotificationQueueMessageItem =
|
type NotificationQueueMessageItem =
|
||||||
| AddLoginQueueMessage
|
| AddLoginQueueMessage
|
||||||
| AddChangePasswordQueueMessage
|
| AddChangePasswordQueueMessage
|
||||||
| AddUnlockVaultQueueMessage;
|
| AddUnlockVaultQueueMessage
|
||||||
|
| AtRiskPasswordQueueMessage;
|
||||||
|
|
||||||
type LockedVaultPendingNotificationsData = {
|
type LockedVaultPendingNotificationsData = {
|
||||||
commandToRetry: {
|
commandToRetry: {
|
||||||
@@ -50,6 +60,13 @@ type LockedVaultPendingNotificationsData = {
|
|||||||
target: string;
|
target: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AtRiskPasswordNotificationsData = {
|
||||||
|
activeUserId: UserId;
|
||||||
|
cipher: CipherView;
|
||||||
|
securityTask: SecurityTask;
|
||||||
|
uri: string;
|
||||||
|
};
|
||||||
|
|
||||||
type AdjustNotificationBarMessageData = {
|
type AdjustNotificationBarMessageData = {
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
@@ -76,7 +93,8 @@ type NotificationBackgroundExtensionMessage = {
|
|||||||
data?: Partial<LockedVaultPendingNotificationsData> &
|
data?: Partial<LockedVaultPendingNotificationsData> &
|
||||||
Partial<AdjustNotificationBarMessageData> &
|
Partial<AdjustNotificationBarMessageData> &
|
||||||
Partial<ChangePasswordMessageData> &
|
Partial<ChangePasswordMessageData> &
|
||||||
Partial<UnlockVaultMessageData>;
|
Partial<UnlockVaultMessageData> &
|
||||||
|
Partial<AtRiskPasswordNotificationsData>;
|
||||||
login?: AddLoginMessageData;
|
login?: AddLoginMessageData;
|
||||||
folder?: string;
|
folder?: string;
|
||||||
edit?: boolean;
|
edit?: boolean;
|
||||||
@@ -101,10 +119,20 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
|||||||
sender,
|
sender,
|
||||||
}: BackgroundOnMessageHandlerParams) => Promise<CollectionView[]>;
|
}: BackgroundOnMessageHandlerParams) => Promise<CollectionView[]>;
|
||||||
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||||
bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
bgOpenAtRiskPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||||
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||||
bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
bgTriggerAddLoginNotification: ({
|
||||||
bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
message,
|
||||||
|
sender,
|
||||||
|
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||||
|
bgTriggerChangedPasswordNotification: ({
|
||||||
|
message,
|
||||||
|
sender,
|
||||||
|
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||||
|
bgTriggerAtRiskPasswordNotification: ({
|
||||||
|
message,
|
||||||
|
sender,
|
||||||
|
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||||
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
|
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
|
||||||
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||||
bgOpenAddEditVaultItemPopout: ({
|
bgOpenAddEditVaultItemPopout: ({
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ describe("NotificationBackground", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("bgAddLogin message handler", () => {
|
describe("bgTriggerAddLoginNotification message handler", () => {
|
||||||
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;
|
||||||
@@ -305,7 +305,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips attempting to add the login if the user is logged out", async () => {
|
it("skips attempting to add the login if the user is logged out", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
|
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
|
||||||
@@ -319,7 +319,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
|
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "" },
|
login: { username: "test", password: "password", url: "" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||||
@@ -333,7 +333,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
|
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||||
@@ -350,7 +350,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
|
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
@@ -368,7 +368,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
@@ -390,7 +390,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
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 message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: { username: "test", password: "password", url: "https://example.com" },
|
login: { username: "test", password: "password", url: "https://example.com" },
|
||||||
};
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
@@ -410,7 +410,10 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("adds the login to the queue if the user has a locked account", async () => {
|
it("adds the login to the queue if the user has a locked account", async () => {
|
||||||
const login = { username: "test", password: "password", url: "https://example.com" };
|
const login = { username: "test", password: "password", url: "https://example.com" };
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
|
command: "bgTriggerAddLoginNotification",
|
||||||
|
login,
|
||||||
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
|
|
||||||
@@ -426,7 +429,10 @@ describe("NotificationBackground", () => {
|
|||||||
password: "password",
|
password: "password",
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
} as any;
|
} as any;
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
|
command: "bgTriggerAddLoginNotification",
|
||||||
|
login,
|
||||||
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||||
@@ -441,7 +447,10 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
|
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
|
||||||
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
||||||
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
|
command: "bgTriggerAddLoginNotification",
|
||||||
|
login,
|
||||||
|
};
|
||||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||||
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
||||||
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
||||||
@@ -464,7 +473,7 @@ describe("NotificationBackground", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("bgChangedPassword 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 pushChangePasswordToQueueSpy: jest.SpyInstance;
|
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||||
@@ -482,7 +491,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
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 message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
|
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -494,7 +503,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
|
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
currentPassword: "currentPassword",
|
currentPassword: "currentPassword",
|
||||||
@@ -517,7 +526,7 @@ 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("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 () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
currentPassword: "currentPassword",
|
currentPassword: "currentPassword",
|
||||||
@@ -538,7 +547,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
|
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
currentPassword: "currentPassword",
|
currentPassword: "currentPassword",
|
||||||
@@ -560,7 +569,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
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 message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
currentPassword: "currentPassword",
|
currentPassword: "currentPassword",
|
||||||
@@ -588,7 +597,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
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("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 () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
@@ -609,7 +618,7 @@ describe("NotificationBackground", () => {
|
|||||||
|
|
||||||
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 () => {
|
||||||
const message: NotificationBackgroundExtensionMessage = {
|
const message: NotificationBackgroundExtensionMessage = {
|
||||||
command: "bgChangedPassword",
|
command: "bgTriggerChangedPasswordNotification",
|
||||||
data: {
|
data: {
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
import { firstValueFrom, switchMap, map, of } from "rxjs";
|
import { firstValueFrom, switchMap, map, of } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import {
|
||||||
|
getOrganizationById,
|
||||||
|
OrganizationService,
|
||||||
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -55,6 +58,7 @@ import {
|
|||||||
import { CollectionView } from "../content/components/common-types";
|
import { CollectionView } from "../content/components/common-types";
|
||||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||||
|
import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddChangePasswordQueueMessage,
|
AddChangePasswordQueueMessage,
|
||||||
@@ -81,14 +85,18 @@ export default class NotificationBackground {
|
|||||||
ExtensionCommand.AutofillIdentity,
|
ExtensionCommand.AutofillIdentity,
|
||||||
]);
|
]);
|
||||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||||
bgAddLogin: ({ message, sender }) => this.addLogin(message, sender),
|
|
||||||
bgAdjustNotificationBar: ({ message, sender }) =>
|
bgAdjustNotificationBar: ({ message, sender }) =>
|
||||||
this.handleAdjustNotificationBarMessage(message, sender),
|
this.handleAdjustNotificationBarMessage(message, sender),
|
||||||
bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender),
|
bgTriggerAddLoginNotification: ({ message, sender }) =>
|
||||||
|
this.triggerAddLoginNotification(message, sender),
|
||||||
|
bgTriggerChangedPasswordNotification: ({ message, sender }) =>
|
||||||
|
this.triggerChangedPasswordNotification(message, sender),
|
||||||
|
bgTriggerAtRiskPasswordNotification: ({ message, sender }) =>
|
||||||
|
this.triggerAtRiskPasswordNotification(message, sender),
|
||||||
bgCloseNotificationBar: ({ message, sender }) =>
|
bgCloseNotificationBar: ({ message, sender }) =>
|
||||||
this.handleCloseNotificationBarMessage(message, sender),
|
this.handleCloseNotificationBarMessage(message, sender),
|
||||||
bgOpenAtRisksPasswords: ({ message, sender }) =>
|
bgOpenAtRiskPasswords: ({ message, sender }) =>
|
||||||
this.handleOpenAtRisksPasswordsMessage(message, sender),
|
this.handleOpenAtRiskPasswordsMessage(message, sender),
|
||||||
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
|
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
|
||||||
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
|
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
|
||||||
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
||||||
@@ -341,12 +349,17 @@ export default class NotificationBackground {
|
|||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
notificationQueueMessage: NotificationQueueMessageItem,
|
notificationQueueMessage: NotificationQueueMessageItem,
|
||||||
) {
|
) {
|
||||||
const notificationType = notificationQueueMessage.type;
|
const {
|
||||||
|
type: notificationType,
|
||||||
|
wasVaultLocked: isVaultLocked,
|
||||||
|
launchTimestamp,
|
||||||
|
...params
|
||||||
|
} = notificationQueueMessage;
|
||||||
|
|
||||||
const typeData: NotificationTypeData = {
|
const typeData: NotificationTypeData = {
|
||||||
isVaultLocked: notificationQueueMessage.wasVaultLocked,
|
isVaultLocked,
|
||||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||||
launchTimestamp: notificationQueueMessage.launchTimestamp,
|
launchTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (notificationType) {
|
switch (notificationType) {
|
||||||
@@ -358,6 +371,7 @@ export default class NotificationBackground {
|
|||||||
await BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
|
await BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
|
||||||
type: notificationType,
|
type: notificationType,
|
||||||
typeData,
|
typeData,
|
||||||
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +389,48 @@ export default class NotificationBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to trigger the at risk password notification
|
||||||
|
*
|
||||||
|
* @param message - The extension message
|
||||||
|
* @param sender - The contextual sender of the message
|
||||||
|
*/
|
||||||
|
async triggerAtRiskPasswordNotification(
|
||||||
|
message: NotificationBackgroundExtensionMessage,
|
||||||
|
sender: chrome.runtime.MessageSender,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { activeUserId, securityTask, cipher } = message.data;
|
||||||
|
const domain = Utils.getDomain(sender.tab.url);
|
||||||
|
const passwordChangeUri =
|
||||||
|
await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher);
|
||||||
|
|
||||||
|
const authStatus = await this.getAuthStatus();
|
||||||
|
|
||||||
|
const wasVaultLocked = authStatus === AuthenticationStatus.Locked;
|
||||||
|
|
||||||
|
const organization = await firstValueFrom(
|
||||||
|
this.organizationService
|
||||||
|
.organizations$(activeUserId)
|
||||||
|
.pipe(getOrganizationById(securityTask.organizationId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.removeTabFromNotificationQueue(sender.tab);
|
||||||
|
const launchTimestamp = new Date().getTime();
|
||||||
|
const queueMessage: NotificationQueueMessageItem = {
|
||||||
|
domain,
|
||||||
|
wasVaultLocked,
|
||||||
|
type: NotificationQueueMessageType.AtRiskPassword,
|
||||||
|
passwordChangeUri,
|
||||||
|
organizationName: organization.name,
|
||||||
|
tab: sender.tab,
|
||||||
|
launchTimestamp,
|
||||||
|
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
|
||||||
|
};
|
||||||
|
this.notificationQueue.push(queueMessage);
|
||||||
|
await this.checkNotificationQueue(sender.tab);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a login message to the notification queue, prompting the user to save
|
* Adds a login message to the notification queue, prompting the user to save
|
||||||
* the login if it does not already exist in the vault. If the cipher exists
|
* the login if it does not already exist in the vault. If the cipher exists
|
||||||
@@ -383,20 +439,20 @@ export default class NotificationBackground {
|
|||||||
* @param message - The message to add to the queue
|
* @param message - The message to add to the queue
|
||||||
* @param sender - The contextual sender of the message
|
* @param sender - The contextual sender of the message
|
||||||
*/
|
*/
|
||||||
async addLogin(
|
async triggerAddLoginNotification(
|
||||||
message: NotificationBackgroundExtensionMessage,
|
message: NotificationBackgroundExtensionMessage,
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) {
|
): Promise<boolean> {
|
||||||
const authStatus = await this.getAuthStatus();
|
const authStatus = await this.getAuthStatus();
|
||||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginInfo = message.login;
|
const loginInfo = message.login;
|
||||||
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
|
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
|
||||||
const loginDomain = Utils.getDomain(loginInfo.url);
|
const loginDomain = Utils.getDomain(loginInfo.url);
|
||||||
if (loginDomain == null) {
|
if (loginDomain == null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
|
const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
|
||||||
@@ -406,14 +462,14 @@ export default class NotificationBackground {
|
|||||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
|
||||||
@@ -422,7 +478,7 @@ export default class NotificationBackground {
|
|||||||
);
|
);
|
||||||
if (addLoginIsEnabled && usernameMatches.length === 0) {
|
if (addLoginIsEnabled && usernameMatches.length === 0) {
|
||||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
|
||||||
@@ -438,7 +494,9 @@ export default class NotificationBackground {
|
|||||||
loginInfo.password,
|
loginInfo.password,
|
||||||
sender.tab,
|
sender.tab,
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pushAddLoginToQueue(
|
private async pushAddLoginToQueue(
|
||||||
@@ -472,14 +530,14 @@ export default class NotificationBackground {
|
|||||||
* @param message - The message to add to the queue
|
* @param message - The message to add to the queue
|
||||||
* @param sender - The contextual sender of the message
|
* @param sender - The contextual sender of the message
|
||||||
*/
|
*/
|
||||||
async changedPassword(
|
async triggerChangedPasswordNotification(
|
||||||
message: NotificationBackgroundExtensionMessage,
|
message: NotificationBackgroundExtensionMessage,
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) {
|
) {
|
||||||
const changeData = message.data as ChangePasswordMessageData;
|
const changeData = message.data as ChangePasswordMessageData;
|
||||||
const loginDomain = Utils.getDomain(changeData.url);
|
const loginDomain = Utils.getDomain(changeData.url);
|
||||||
if (loginDomain == null) {
|
if (loginDomain == null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||||
@@ -490,7 +548,7 @@ export default class NotificationBackground {
|
|||||||
sender.tab,
|
sender.tab,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id: string = null;
|
let id: string = null;
|
||||||
@@ -498,7 +556,7 @@ export default class NotificationBackground {
|
|||||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||||
);
|
);
|
||||||
if (activeUserId == null) {
|
if (activeUserId == null) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
|
||||||
@@ -514,7 +572,9 @@ export default class NotificationBackground {
|
|||||||
}
|
}
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
|
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -900,7 +960,7 @@ export default class NotificationBackground {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSecurityTasks(userId: UserId) {
|
async getSecurityTasks(userId: UserId) {
|
||||||
let tasks: SecurityTask[] = [];
|
let tasks: SecurityTask[] = [];
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@@ -1074,7 +1134,7 @@ export default class NotificationBackground {
|
|||||||
* @param message - The extension message
|
* @param message - The extension message
|
||||||
* @param sender - The contextual sender of the message
|
* @param sender - The contextual sender of the message
|
||||||
*/
|
*/
|
||||||
private async handleOpenAtRisksPasswordsMessage(
|
private async handleOpenAtRiskPasswordsMessage(
|
||||||
message: NotificationBackgroundExtensionMessage,
|
message: NotificationBackgroundExtensionMessage,
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
|
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import AutofillField from "../models/autofill-field";
|
import AutofillField from "../models/autofill-field";
|
||||||
@@ -24,6 +27,9 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
|
|||||||
describe("OverlayNotificationsBackground", () => {
|
describe("OverlayNotificationsBackground", () => {
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
let notificationBackground: NotificationBackground;
|
let notificationBackground: NotificationBackground;
|
||||||
|
let taskService: TaskService;
|
||||||
|
let accountService: AccountService;
|
||||||
|
let cipherService: CipherService;
|
||||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||||
let overlayNotificationsBackground: OverlayNotificationsBackground;
|
let overlayNotificationsBackground: OverlayNotificationsBackground;
|
||||||
@@ -32,6 +38,9 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
logService = mock<LogService>();
|
logService = mock<LogService>();
|
||||||
notificationBackground = mock<NotificationBackground>();
|
notificationBackground = mock<NotificationBackground>();
|
||||||
|
taskService = mock<TaskService>();
|
||||||
|
accountService = mock<AccountService>();
|
||||||
|
cipherService = mock<CipherService>();
|
||||||
getEnableChangedPasswordPromptSpy = jest
|
getEnableChangedPasswordPromptSpy = jest
|
||||||
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
||||||
.mockResolvedValue(true);
|
.mockResolvedValue(true);
|
||||||
@@ -41,6 +50,9 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
overlayNotificationsBackground = new OverlayNotificationsBackground(
|
overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||||
logService,
|
logService,
|
||||||
notificationBackground,
|
notificationBackground,
|
||||||
|
taskService,
|
||||||
|
accountService,
|
||||||
|
cipherService,
|
||||||
);
|
);
|
||||||
await overlayNotificationsBackground.init();
|
await overlayNotificationsBackground.init();
|
||||||
});
|
});
|
||||||
@@ -329,8 +341,11 @@ describe("OverlayNotificationsBackground", () => {
|
|||||||
tab: { id: 1 },
|
tab: { id: 1 },
|
||||||
url: "https://example.com",
|
url: "https://example.com",
|
||||||
});
|
});
|
||||||
notificationChangedPasswordSpy = jest.spyOn(notificationBackground, "changedPassword");
|
notificationChangedPasswordSpy = jest.spyOn(
|
||||||
notificationAddLoginSpy = jest.spyOn(notificationBackground, "addLogin");
|
notificationBackground,
|
||||||
|
"triggerChangedPasswordNotification",
|
||||||
|
);
|
||||||
|
notificationAddLoginSpy = jest.spyOn(notificationBackground, "triggerAddLoginNotification");
|
||||||
|
|
||||||
sendMockExtensionMessage(
|
sendMockExtensionMessage(
|
||||||
{ command: "collectPageDetailsResponse", details: pageDetails },
|
{ command: "collectPageDetailsResponse", details: pageDetails },
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Subject, switchMap, timer } from "rxjs";
|
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/common/vault/tasks";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
||||||
@@ -19,6 +25,12 @@ import {
|
|||||||
} from "./abstractions/overlay-notifications.background";
|
} from "./abstractions/overlay-notifications.background";
|
||||||
import NotificationBackground from "./notification.background";
|
import NotificationBackground from "./notification.background";
|
||||||
|
|
||||||
|
type LoginSecurityTaskInfo = {
|
||||||
|
securityTask: SecurityTask;
|
||||||
|
cipher: CipherView;
|
||||||
|
uri: ModifyLoginCipherFormData["uri"];
|
||||||
|
};
|
||||||
|
|
||||||
export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface {
|
export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface {
|
||||||
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
|
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
|
||||||
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
|
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
|
||||||
@@ -35,6 +47,9 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
constructor(
|
constructor(
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private notificationBackground: NotificationBackground,
|
private notificationBackground: NotificationBackground,
|
||||||
|
private taskService: TaskService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private cipherService: CipherService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,8 +274,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
|
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
|
||||||
return (
|
return (
|
||||||
!modifyLoginData ||
|
!modifyLoginData ||
|
||||||
!this.shouldTriggerAddLoginNotification(modifyLoginData) ||
|
!this.shouldAttemptAddLoginNotification(modifyLoginData) ||
|
||||||
!this.shouldTriggerChangePasswordNotification(modifyLoginData)
|
!this.shouldAttemptChangedPasswordNotification(modifyLoginData)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -404,10 +419,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
modifyLoginData: ModifyLoginCipherFormData,
|
modifyLoginData: ModifyLoginCipherFormData,
|
||||||
tab: chrome.tabs.Tab,
|
tab: chrome.tabs.Tab,
|
||||||
) => {
|
) => {
|
||||||
if (this.shouldTriggerChangePasswordNotification(modifyLoginData)) {
|
let result: string;
|
||||||
|
if (this.shouldAttemptChangedPasswordNotification(modifyLoginData)) {
|
||||||
// These notifications are temporarily setup as "messages" to the notification background.
|
// These notifications are temporarily setup as "messages" to the notification background.
|
||||||
// This will be structured differently in a future refactor.
|
// This will be structured differently in a future refactor.
|
||||||
await this.notificationBackground.changedPassword(
|
const success = await this.notificationBackground.triggerChangedPasswordNotification(
|
||||||
{
|
{
|
||||||
command: "bgChangedPassword",
|
command: "bgChangedPassword",
|
||||||
data: {
|
data: {
|
||||||
@@ -418,14 +434,15 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
},
|
},
|
||||||
{ tab },
|
{ tab },
|
||||||
);
|
);
|
||||||
this.clearCompletedWebRequest(requestId, tab);
|
if (!success) {
|
||||||
return;
|
result = "Unqualified changedPassword notification attempt.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldTriggerAddLoginNotification(modifyLoginData)) {
|
if (this.shouldAttemptAddLoginNotification(modifyLoginData)) {
|
||||||
await this.notificationBackground.addLogin(
|
const success = await this.notificationBackground.triggerAddLoginNotification(
|
||||||
{
|
{
|
||||||
command: "bgAddLogin",
|
command: "bgTriggerAddLoginNotification",
|
||||||
login: {
|
login: {
|
||||||
url: modifyLoginData.uri,
|
url: modifyLoginData.uri,
|
||||||
username: modifyLoginData.username,
|
username: modifyLoginData.username,
|
||||||
@@ -434,8 +451,44 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
},
|
},
|
||||||
{ tab },
|
{ tab },
|
||||||
);
|
);
|
||||||
this.clearCompletedWebRequest(requestId, tab);
|
if (!success) {
|
||||||
|
result = "Unqualified addLogin notification attempt.";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldGetTasks =
|
||||||
|
(await this.notificationBackground.getNotificationFlag()) && !modifyLoginData.newPassword;
|
||||||
|
|
||||||
|
if (shouldGetTasks) {
|
||||||
|
const activeUserId = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeUserId) {
|
||||||
|
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
|
||||||
|
modifyLoginData,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loginSecurityTaskInfo) {
|
||||||
|
await this.notificationBackground.triggerAtRiskPasswordNotification(
|
||||||
|
{
|
||||||
|
command: "bgTriggerAtRiskPasswordNotification",
|
||||||
|
data: {
|
||||||
|
activeUserId,
|
||||||
|
cipher: loginSecurityTaskInfo.cipher,
|
||||||
|
securityTask: loginSecurityTaskInfo.securityTask,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ tab },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = "Unqualified atRiskPassword notification attempt.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clearCompletedWebRequest(requestId, tab);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -443,7 +496,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
*
|
*
|
||||||
* @param modifyLoginData - The modified login form data
|
* @param modifyLoginData - The modified login form data
|
||||||
*/
|
*/
|
||||||
private shouldTriggerChangePasswordNotification = (
|
private shouldAttemptChangedPasswordNotification = (
|
||||||
modifyLoginData: ModifyLoginCipherFormData,
|
modifyLoginData: ModifyLoginCipherFormData,
|
||||||
) => {
|
) => {
|
||||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||||
@@ -454,10 +507,66 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
|||||||
*
|
*
|
||||||
* @param modifyLoginData - The modified login form data
|
* @param modifyLoginData - The modified login form data
|
||||||
*/
|
*/
|
||||||
private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
private shouldAttemptAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
||||||
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
|
||||||
|
*
|
||||||
|
* @param modifyLoginData - The modified login form data
|
||||||
|
* @param activeUserId - The currently logged in user ID
|
||||||
|
*/
|
||||||
|
private async getSecurityTaskAndCipherForLoginData(
|
||||||
|
modifyLoginData: ModifyLoginCipherFormData,
|
||||||
|
activeUserId: UserId,
|
||||||
|
): Promise<LoginSecurityTaskInfo | null> {
|
||||||
|
const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId);
|
||||||
|
if (!tasks?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||||
|
modifyLoginData.uri,
|
||||||
|
activeUserId,
|
||||||
|
);
|
||||||
|
if (!urlCiphers?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const securityTaskForLogin = urlCiphers.reduce(
|
||||||
|
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
|
||||||
|
if (
|
||||||
|
// exit early if info was found already
|
||||||
|
taskInfo ||
|
||||||
|
// exit early if the cipher was deleted
|
||||||
|
cipher.deletedDate ||
|
||||||
|
// exit early if the entered login info doesn't match an existing cipher
|
||||||
|
modifyLoginData.username !== cipher.login.username ||
|
||||||
|
modifyLoginData.password !== cipher.login.password
|
||||||
|
) {
|
||||||
|
return taskInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the first security task for the cipherId belonging to the entered login
|
||||||
|
const cipherSecurityTask = tasks.find(
|
||||||
|
({ cipherId, status }) =>
|
||||||
|
cipher.id === cipherId && // match security task cipher id to url cipher id
|
||||||
|
status === SecurityTaskStatus.Pending, // security task has not been completed
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cipherSecurityTask) {
|
||||||
|
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskInfo;
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return securityTaskForLogin;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the completed web request and removes the modified login form data for the tab.
|
* Clears the completed web request and removes the modified login form data for the tab.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type ActionButtonProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
handleClick: (e: Event) => void;
|
handleClick: (e: Event) => void;
|
||||||
|
fullWidth?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ActionButton({
|
export function ActionButton({
|
||||||
@@ -17,6 +18,7 @@ export function ActionButton({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
theme,
|
theme,
|
||||||
handleClick,
|
handleClick,
|
||||||
|
fullWidth = true,
|
||||||
}: ActionButtonProps) {
|
}: ActionButtonProps) {
|
||||||
const handleButtonClick = (event: Event) => {
|
const handleButtonClick = (event: Event) => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
@@ -26,7 +28,7 @@ export function ActionButton({
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
class=${actionButtonStyles({ disabled, theme })}
|
class=${actionButtonStyles({ disabled, theme, fullWidth })}
|
||||||
title=${buttonText}
|
title=${buttonText}
|
||||||
type="button"
|
type="button"
|
||||||
@click=${handleButtonClick}
|
@click=${handleButtonClick}
|
||||||
@@ -36,14 +38,22 @@ export function ActionButton({
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css`
|
const actionButtonStyles = ({
|
||||||
|
disabled,
|
||||||
|
theme,
|
||||||
|
fullWidth,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
theme: Theme;
|
||||||
|
fullWidth: boolean;
|
||||||
|
}) => css`
|
||||||
${typography.body2}
|
${typography.body2}
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: ${border.radius.full};
|
border-radius: ${border.radius.full};
|
||||||
padding: ${spacing["1"]} ${spacing["3"]};
|
padding: ${spacing["1"]} ${spacing["3"]};
|
||||||
width: 100%;
|
width: ${fullWidth ? "100%" : "auto"};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { spacing, themes } from "../../constants/styles";
|
||||||
|
import { ExternalLink } from "../../icons";
|
||||||
|
|
||||||
|
export function AdditionalTasksButtonContent({
|
||||||
|
buttonText,
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
buttonText: string;
|
||||||
|
theme: Theme;
|
||||||
|
}) {
|
||||||
|
return html`
|
||||||
|
<div class=${additionalTasksButtonContentStyles({ theme })}>
|
||||||
|
<span>${buttonText}</span>
|
||||||
|
${ExternalLink({ theme, color: themes[theme].text.contrast })}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css`
|
||||||
|
gap: ${spacing[2]};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
@@ -103,6 +103,12 @@ export const mockTasks = [
|
|||||||
|
|
||||||
export const mockI18n = {
|
export const mockI18n = {
|
||||||
appName: "Bitwarden",
|
appName: "Bitwarden",
|
||||||
|
atRiskPassword: "At-risk password",
|
||||||
|
atRiskNavigatePrompt:
|
||||||
|
"$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.",
|
||||||
|
atRiskChangePrompt:
|
||||||
|
"Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.",
|
||||||
|
changePassword: "Change password",
|
||||||
close: "Close",
|
close: "Close",
|
||||||
collection: "Collection",
|
collection: "Collection",
|
||||||
folder: "Folder",
|
folder: "Folder",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Meta, StoryObj } from "@storybook/web-components";
|
||||||
|
|
||||||
|
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AtRiskNotification,
|
||||||
|
AtRiskNotificationProps,
|
||||||
|
} from "../../../notification/at-risk-password/container";
|
||||||
|
import { mockI18n, mockBrowserI18nGetMessage } from "../../mock-data";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Notifications/At-Risk Notification",
|
||||||
|
argTypes: {
|
||||||
|
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
theme: ThemeTypes.Light,
|
||||||
|
handleCloseNotification: () => alert("Close notification action triggered"),
|
||||||
|
params: {
|
||||||
|
passwordChangeUri: "https://webtests.dev/.well-known/change-password", // Remove to see "navigate" version of notification
|
||||||
|
organizationName: "Acme Co.",
|
||||||
|
},
|
||||||
|
i18n: mockI18n,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta<AtRiskNotificationProps>;
|
||||||
|
|
||||||
|
const Template = (args: AtRiskNotificationProps) => AtRiskNotification({ ...args });
|
||||||
|
|
||||||
|
export const Default: StoryObj<AtRiskNotificationProps> = {
|
||||||
|
render: Template,
|
||||||
|
};
|
||||||
|
|
||||||
|
window.chrome = {
|
||||||
|
...window.chrome,
|
||||||
|
i18n: {
|
||||||
|
getMessage: mockBrowserI18nGetMessage,
|
||||||
|
},
|
||||||
|
} as typeof chrome;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import createEmotion from "@emotion/css/create-instance";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { spacing, themes } from "../../constants/styles";
|
||||||
|
import { Warning } from "../../illustrations";
|
||||||
|
|
||||||
|
import { AtRiskNotificationMessage } from "./message";
|
||||||
|
|
||||||
|
export const componentClassPrefix = "at-risk-notification-body";
|
||||||
|
|
||||||
|
const { css } = createEmotion({
|
||||||
|
key: componentClassPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AtRiskNotificationBodyProps = {
|
||||||
|
riskMessage: string;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AtRiskNotificationBody({ riskMessage, theme }: AtRiskNotificationBodyProps) {
|
||||||
|
return html`
|
||||||
|
<div class=${atRiskNotificationBodyStyles({ theme })}>
|
||||||
|
<div class=${iconContainerStyles}>${Warning()}</div>
|
||||||
|
${riskMessage
|
||||||
|
? AtRiskNotificationMessage({
|
||||||
|
message: riskMessage,
|
||||||
|
theme,
|
||||||
|
})
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconContainerStyles = css`
|
||||||
|
> svg {
|
||||||
|
width: 50px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const atRiskNotificationBodyStyles = ({ theme }: { theme: Theme }) => css`
|
||||||
|
gap: ${spacing[4]};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
background-color: ${themes[theme].background.alt};
|
||||||
|
padding: 12px;
|
||||||
|
`;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { NotificationBarIframeInitData } from "../../../../notification/abstractions/notification-bar";
|
||||||
|
import { I18n } from "../../common-types";
|
||||||
|
import { themes, spacing } from "../../constants/styles";
|
||||||
|
import {
|
||||||
|
NotificationHeader,
|
||||||
|
componentClassPrefix as notificationHeaderClassPrefix,
|
||||||
|
} from "../header";
|
||||||
|
|
||||||
|
import { AtRiskNotificationBody } from "./body";
|
||||||
|
import { AtRiskNotificationFooter } from "./footer";
|
||||||
|
|
||||||
|
export type AtRiskNotificationProps = NotificationBarIframeInitData & {
|
||||||
|
handleCloseNotification: (e: Event) => void;
|
||||||
|
} & {
|
||||||
|
i18n: I18n;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AtRiskNotification({
|
||||||
|
handleCloseNotification,
|
||||||
|
i18n,
|
||||||
|
theme = ThemeTypes.Light,
|
||||||
|
params,
|
||||||
|
}: AtRiskNotificationProps) {
|
||||||
|
const { passwordChangeUri, organizationName } = params;
|
||||||
|
const riskMessage = chrome.i18n.getMessage(
|
||||||
|
passwordChangeUri ? "atRiskChangePrompt" : "atRiskNavigatePrompt",
|
||||||
|
organizationName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class=${atRiskNotificationContainerStyles(theme)}>
|
||||||
|
${NotificationHeader({
|
||||||
|
handleCloseNotification,
|
||||||
|
i18n,
|
||||||
|
message: i18n.atRiskPassword,
|
||||||
|
theme,
|
||||||
|
})}
|
||||||
|
${AtRiskNotificationBody({
|
||||||
|
theme,
|
||||||
|
riskMessage,
|
||||||
|
})}
|
||||||
|
${passwordChangeUri
|
||||||
|
? AtRiskNotificationFooter({
|
||||||
|
i18n,
|
||||||
|
theme,
|
||||||
|
passwordChangeUri: params?.passwordChangeUri,
|
||||||
|
})
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const atRiskNotificationContainerStyles = (theme: Theme) => css`
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
border: 1px solid ${themes[theme].secondary["300"]};
|
||||||
|
border-radius: ${spacing["4"]};
|
||||||
|
box-shadow: -2px 4px 6px 0px #0000001a;
|
||||||
|
background-color: ${themes[theme].background.alt};
|
||||||
|
width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
[class*="${notificationHeaderClassPrefix}-"] {
|
||||||
|
border-radius: ${spacing["4"]} ${spacing["4"]} 0 0;
|
||||||
|
border-bottom: 0.5px solid ${themes[theme].secondary["300"]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { ActionButton } from "../../buttons/action-button";
|
||||||
|
import { AdditionalTasksButtonContent } from "../../buttons/additional-tasks/button-content";
|
||||||
|
import { I18n } from "../../common-types";
|
||||||
|
import { spacing } from "../../constants/styles";
|
||||||
|
|
||||||
|
export type AtRiskNotificationFooterProps = {
|
||||||
|
i18n: I18n;
|
||||||
|
theme: Theme;
|
||||||
|
passwordChangeUri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AtRiskNotificationFooter({
|
||||||
|
i18n,
|
||||||
|
theme,
|
||||||
|
passwordChangeUri,
|
||||||
|
}: AtRiskNotificationFooterProps) {
|
||||||
|
return html`<div class=${atRiskNotificationFooterStyles}>
|
||||||
|
${passwordChangeUri &&
|
||||||
|
ActionButton({
|
||||||
|
handleClick: () => {
|
||||||
|
open(passwordChangeUri, "_blank");
|
||||||
|
},
|
||||||
|
buttonText: AdditionalTasksButtonContent({ buttonText: i18n.changePassword, theme }),
|
||||||
|
theme,
|
||||||
|
fullWidth: false,
|
||||||
|
})}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const atRiskNotificationFooterStyles = css`
|
||||||
|
display: flex;
|
||||||
|
padding: ${spacing[2]} ${spacing[4]} ${spacing[4]} ${spacing[4]};
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
border-radius: 0 0 ${spacing["4"]} ${spacing["4"]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { css } from "@emotion/css";
|
||||||
|
import { html, nothing } from "lit";
|
||||||
|
|
||||||
|
import { Theme } from "@bitwarden/common/platform/enums";
|
||||||
|
|
||||||
|
import { themes } from "../../constants/styles";
|
||||||
|
|
||||||
|
export type AtRiskNotificationMessageProps = {
|
||||||
|
message?: string;
|
||||||
|
theme: Theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AtRiskNotificationMessage({ message, theme }: AtRiskNotificationMessageProps) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
${message
|
||||||
|
? html`
|
||||||
|
<span title=${message} class=${atRiskNotificationMessageStyles(theme)}>
|
||||||
|
${message}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTextStyles = css`
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 24px;
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const atRiskNotificationMessageStyles = (theme: Theme) => css`
|
||||||
|
${baseTextStyles}
|
||||||
|
|
||||||
|
color: ${themes[theme].text.main};
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
display: inline;
|
||||||
|
`;
|
||||||
@@ -2,6 +2,7 @@ const NotificationQueueMessageType = {
|
|||||||
AddLogin: "add",
|
AddLogin: "add",
|
||||||
ChangePassword: "change",
|
ChangePassword: "change",
|
||||||
UnlockVault: "unlock",
|
UnlockVault: "unlock",
|
||||||
|
AtRiskPassword: "at-risk-password",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type NotificationQueueMessageTypes =
|
type NotificationQueueMessageTypes =
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const NotificationTypes = {
|
|||||||
Add: "add",
|
Add: "add",
|
||||||
Change: "change",
|
Change: "change",
|
||||||
Unlock: "unlock",
|
Unlock: "unlock",
|
||||||
|
AtRiskPassword: "at-risk-password",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||||
@@ -30,7 +31,8 @@ type NotificationBarIframeInitData = {
|
|||||||
organizations?: OrgView[];
|
organizations?: OrgView[];
|
||||||
removeIndividualVault?: boolean;
|
removeIndividualVault?: boolean;
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
type?: string; // @TODO use `NotificationType`
|
type?: NotificationType;
|
||||||
|
params?: AtRiskPasswordNotificationParams | any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type NotificationBarWindowMessage = {
|
type NotificationBarWindowMessage = {
|
||||||
@@ -50,7 +52,13 @@ type NotificationBarWindowMessageHandlers = {
|
|||||||
saveCipherAttemptCompleted: ({ message }: { message: NotificationBarWindowMessage }) => void;
|
saveCipherAttemptCompleted: ({ message }: { message: NotificationBarWindowMessage }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AtRiskPasswordNotificationParams = {
|
||||||
|
passwordChangeUri?: string;
|
||||||
|
organizationName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AtRiskPasswordNotificationParams,
|
||||||
NotificationTaskInfo,
|
NotificationTaskInfo,
|
||||||
NotificationTypes,
|
NotificationTypes,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view
|
|||||||
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
|
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
|
||||||
import { NotificationCipherData } from "../content/components/cipher/types";
|
import { NotificationCipherData } from "../content/components/cipher/types";
|
||||||
import { CollectionView, I18n, OrgView } from "../content/components/common-types";
|
import { CollectionView, I18n, OrgView } from "../content/components/common-types";
|
||||||
|
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 { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
||||||
@@ -56,21 +57,24 @@ function applyNotificationBarStyle() {
|
|||||||
function getI18n() {
|
function getI18n() {
|
||||||
return {
|
return {
|
||||||
appName: chrome.i18n.getMessage("appName"),
|
appName: chrome.i18n.getMessage("appName"),
|
||||||
|
atRiskPassword: chrome.i18n.getMessage("atRiskPassword"),
|
||||||
|
changePassword: chrome.i18n.getMessage("changePassword"),
|
||||||
close: chrome.i18n.getMessage("close"),
|
close: chrome.i18n.getMessage("close"),
|
||||||
collection: chrome.i18n.getMessage("collection"),
|
collection: chrome.i18n.getMessage("collection"),
|
||||||
folder: chrome.i18n.getMessage("folder"),
|
folder: chrome.i18n.getMessage("folder"),
|
||||||
|
loginSaveConfirmation: chrome.i18n.getMessage("loginSaveConfirmation"),
|
||||||
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
|
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
|
||||||
|
loginUpdatedConfirmation: chrome.i18n.getMessage("loginUpdatedConfirmation"),
|
||||||
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
|
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
|
||||||
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
|
loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"),
|
||||||
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
|
loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"),
|
||||||
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
|
|
||||||
newItem: chrome.i18n.getMessage("newItem"),
|
|
||||||
never: chrome.i18n.getMessage("never"),
|
|
||||||
myVault: chrome.i18n.getMessage("myVault"),
|
myVault: chrome.i18n.getMessage("myVault"),
|
||||||
|
never: chrome.i18n.getMessage("never"),
|
||||||
|
newItem: chrome.i18n.getMessage("newItem"),
|
||||||
|
nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"),
|
||||||
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
|
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
|
||||||
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
||||||
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||||
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
|
|
||||||
notificationEdit: chrome.i18n.getMessage("edit"),
|
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||||
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
|
notificationEditTooltip: chrome.i18n.getMessage("notificationEditTooltip"),
|
||||||
notificationLoginSaveConfirmation: chrome.i18n.getMessage("notificationLoginSaveConfirmation"),
|
notificationLoginSaveConfirmation: chrome.i18n.getMessage("notificationLoginSaveConfirmation"),
|
||||||
@@ -79,6 +83,7 @@ function getI18n() {
|
|||||||
),
|
),
|
||||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||||
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
||||||
|
notificationUpdate: chrome.i18n.getMessage("notificationChangeSave"),
|
||||||
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
|
notificationViewAria: chrome.i18n.getMessage("notificationViewAria"),
|
||||||
saveAction: chrome.i18n.getMessage("notificationAddSave"),
|
saveAction: chrome.i18n.getMessage("notificationAddSave"),
|
||||||
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
|
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
|
||||||
@@ -87,8 +92,8 @@ function getI18n() {
|
|||||||
saveLogin: chrome.i18n.getMessage("saveLogin"),
|
saveLogin: chrome.i18n.getMessage("saveLogin"),
|
||||||
typeLogin: chrome.i18n.getMessage("typeLogin"),
|
typeLogin: chrome.i18n.getMessage("typeLogin"),
|
||||||
unlockToSave: chrome.i18n.getMessage("unlockToSave"),
|
unlockToSave: chrome.i18n.getMessage("unlockToSave"),
|
||||||
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
|
|
||||||
updateLogin: chrome.i18n.getMessage("updateLogin"),
|
updateLogin: chrome.i18n.getMessage("updateLogin"),
|
||||||
|
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
|
||||||
vault: chrome.i18n.getMessage("vault"),
|
vault: chrome.i18n.getMessage("vault"),
|
||||||
view: chrome.i18n.getMessage("view"),
|
view: chrome.i18n.getMessage("view"),
|
||||||
};
|
};
|
||||||
@@ -124,6 +129,7 @@ export function getNotificationHeaderMessage(i18n: I18n, type?: NotificationType
|
|||||||
[NotificationTypes.Add]: i18n.saveLogin,
|
[NotificationTypes.Add]: i18n.saveLogin,
|
||||||
[NotificationTypes.Change]: i18n.updateLogin,
|
[NotificationTypes.Change]: i18n.updateLogin,
|
||||||
[NotificationTypes.Unlock]: i18n.unlockToSave,
|
[NotificationTypes.Unlock]: i18n.unlockToSave,
|
||||||
|
[NotificationTypes.AtRiskPassword]: i18n.atRiskPassword,
|
||||||
}[type]
|
}[type]
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
@@ -143,6 +149,7 @@ export function getConfirmationHeaderMessage(i18n: I18n, type?: NotificationType
|
|||||||
[NotificationTypes.Add]: i18n.loginSaveSuccess,
|
[NotificationTypes.Add]: i18n.loginSaveSuccess,
|
||||||
[NotificationTypes.Change]: i18n.loginUpdateSuccess,
|
[NotificationTypes.Change]: i18n.loginUpdateSuccess,
|
||||||
[NotificationTypes.Unlock]: "",
|
[NotificationTypes.Unlock]: "",
|
||||||
|
[NotificationTypes.AtRiskPassword]: "",
|
||||||
}[type]
|
}[type]
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
@@ -193,6 +200,7 @@ export function getNotificationTestId(
|
|||||||
[NotificationTypes.Unlock]: "unlock-notification-bar",
|
[NotificationTypes.Unlock]: "unlock-notification-bar",
|
||||||
[NotificationTypes.Add]: "save-notification-bar",
|
[NotificationTypes.Add]: "save-notification-bar",
|
||||||
[NotificationTypes.Change]: "update-notification-bar",
|
[NotificationTypes.Change]: "update-notification-bar",
|
||||||
|
[NotificationTypes.AtRiskPassword]: "at-risk-password-notification-bar",
|
||||||
}[notificationType];
|
}[notificationType];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +270,24 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle AtRiskPasswordNotification render
|
||||||
|
if (notificationBarIframeInitData.type === NotificationTypes.AtRiskPassword) {
|
||||||
|
return render(
|
||||||
|
AtRiskNotification({
|
||||||
|
...notificationBarIframeInitData,
|
||||||
|
type: notificationBarIframeInitData.type as NotificationType,
|
||||||
|
theme: resolvedTheme,
|
||||||
|
i18n,
|
||||||
|
params: initData.params,
|
||||||
|
handleCloseNotification,
|
||||||
|
}),
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default scenario: add or update password
|
||||||
const orgId = selectedVaultSignal.get();
|
const orgId = selectedVaultSignal.get();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
new Promise<OrgView[]>((resolve) =>
|
new Promise<OrgView[]>((resolve) =>
|
||||||
sendPlatformMessage({ command: "bgGetOrgData" }, resolve),
|
sendPlatformMessage({ command: "bgGetOrgData" }, resolve),
|
||||||
@@ -533,7 +558,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
|||||||
...notificationBarIframeInitData,
|
...notificationBarIframeInitData,
|
||||||
error,
|
error,
|
||||||
handleCloseNotification,
|
handleCloseNotification,
|
||||||
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }),
|
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRiskPasswords" }),
|
||||||
handleOpenVault: (e: Event) =>
|
handleOpenVault: (e: Event) =>
|
||||||
cipherId ? openViewVaultItemPopout(cipherId) : openAddEditVaultItemPopout(e, {}),
|
cipherId ? openViewVaultItemPopout(cipherId) : openAddEditVaultItemPopout(e, {}),
|
||||||
headerMessage,
|
headerMessage,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type NotificationsExtensionMessage = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
closedByUser?: boolean;
|
closedByUser?: boolean;
|
||||||
fadeOutNotification?: boolean;
|
fadeOutNotification?: boolean;
|
||||||
|
params: object;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||||
|
|
||||||
import { NotificationBarIframeInitData } from "../../../notification/abstractions/notification-bar";
|
import {
|
||||||
|
NotificationBarIframeInitData,
|
||||||
|
NotificationType,
|
||||||
|
NotificationTypes,
|
||||||
|
} from "../../../notification/abstractions/notification-bar";
|
||||||
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
import { sendExtensionMessage, setElementStyles } from "../../../utils";
|
||||||
import {
|
import {
|
||||||
NotificationsExtensionMessage,
|
NotificationsExtensionMessage,
|
||||||
@@ -15,8 +19,7 @@ export class OverlayNotificationsContentService
|
|||||||
{
|
{
|
||||||
private notificationBarElement: HTMLElement | null = null;
|
private notificationBarElement: HTMLElement | null = null;
|
||||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||||
private currentNotificationBarType: string | null = null;
|
private currentNotificationBarType: NotificationType | null = null;
|
||||||
private removeTabFromNotificationQueueTypes = new Set(["add", "change"]);
|
|
||||||
private notificationRefreshFlag: boolean = false;
|
private notificationRefreshFlag: boolean = false;
|
||||||
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
|
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
|
||||||
height: "82px",
|
height: "82px",
|
||||||
@@ -79,17 +82,19 @@ export class OverlayNotificationsContentService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, typeData } = message.data;
|
const { type, typeData, params } = message.data;
|
||||||
|
|
||||||
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
|
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
|
||||||
this.closeNotificationBar();
|
this.closeNotificationBar();
|
||||||
}
|
}
|
||||||
const initData = {
|
const initData = {
|
||||||
type,
|
type: type as NotificationType,
|
||||||
isVaultLocked: typeData.isVaultLocked,
|
isVaultLocked: typeData.isVaultLocked,
|
||||||
theme: typeData.theme,
|
theme: typeData.theme,
|
||||||
removeIndividualVault: typeData.removeIndividualVault,
|
removeIndividualVault: typeData.removeIndividualVault,
|
||||||
importType: typeData.importType,
|
importType: typeData.importType,
|
||||||
launchTimestamp: typeData.launchTimestamp,
|
launchTimestamp: typeData.launchTimestamp,
|
||||||
|
params,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (globalThis.document.readyState === "loading") {
|
if (globalThis.document.readyState === "loading") {
|
||||||
@@ -291,10 +296,13 @@ export class OverlayNotificationsContentService
|
|||||||
this.notificationBarElement.remove();
|
this.notificationBarElement.remove();
|
||||||
this.notificationBarElement = null;
|
this.notificationBarElement = null;
|
||||||
|
|
||||||
if (
|
const removableNotificationTypes = new Set([
|
||||||
closedByUserAction &&
|
NotificationTypes.Add,
|
||||||
this.removeTabFromNotificationQueueTypes.has(this.currentNotificationBarType)
|
NotificationTypes.Change,
|
||||||
) {
|
NotificationTypes.AtRiskPassword,
|
||||||
|
] as NotificationType[]);
|
||||||
|
|
||||||
|
if (closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType)) {
|
||||||
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
|
// Duplicates Default Change Login Password Service, for now
|
||||||
|
// Since the former is an Angular injectable service, and we
|
||||||
|
// need to use the function inside of lit components.
|
||||||
|
// If primary service can be abstracted, that would be ideal.
|
||||||
|
|
||||||
|
export class TemporaryNotificationChangeLoginService {
|
||||||
|
async getChangePasswordUrl(cipher: CipherView, fallback = false): Promise<string | null> {
|
||||||
|
// Ensure we have a cipher with at least one URI
|
||||||
|
if (cipher.type !== CipherType.Login || cipher.login == null || !cipher.login.hasUris) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter for valid URLs that are HTTP(S)
|
||||||
|
const urls = cipher.login.uris
|
||||||
|
.map((m) => Utils.getUrl(m.uri))
|
||||||
|
.filter((m) => m != null && (m.protocol === "http:" || m.protocol === "https:"));
|
||||||
|
|
||||||
|
if (urls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const url of urls) {
|
||||||
|
const [reliable, wellKnownChangeUrl] = await Promise.all([
|
||||||
|
this.hasReliableHttpStatusCode(url.origin),
|
||||||
|
this.getWellKnownChangePasswordUrl(url.origin),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Some servers return a 200 OK for a resource that should not exist
|
||||||
|
// Which means we cannot trust the well-known URL is valid, so we skip it
|
||||||
|
// to avoid potentially sending users to a 404 page
|
||||||
|
if (reliable && wellKnownChangeUrl != null) {
|
||||||
|
return wellKnownChangeUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No reliable well-known URL found, fallback to the first URL
|
||||||
|
|
||||||
|
// @TODO reimplement option in original service to indicate if no URL found.
|
||||||
|
// return urls[0].href; (originally)
|
||||||
|
return fallback ? urls[0].href : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the server returns a non-200 status code for a resource that should not exist.
|
||||||
|
* See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics
|
||||||
|
* @param urlOrigin The origin of the URL to check
|
||||||
|
*/
|
||||||
|
private async hasReliableHttpStatusCode(urlOrigin: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const url = new URL(
|
||||||
|
"./.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200",
|
||||||
|
urlOrigin,
|
||||||
|
);
|
||||||
|
|
||||||
|
const request = new Request(url, {
|
||||||
|
method: "GET",
|
||||||
|
mode: "same-origin",
|
||||||
|
credentials: "omit",
|
||||||
|
cache: "no-store",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(request);
|
||||||
|
return !response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response
|
||||||
|
* is returned. Returns null if the request throws or the response is not 200 OK.
|
||||||
|
* See https://w3c.github.io/webappsec-change-password-url/
|
||||||
|
* @param urlOrigin The origin of the URL to check
|
||||||
|
*/
|
||||||
|
private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const url = new URL("./.well-known/change-password", urlOrigin);
|
||||||
|
|
||||||
|
const request = new Request(url, {
|
||||||
|
method: "GET",
|
||||||
|
mode: "same-origin",
|
||||||
|
credentials: "omit",
|
||||||
|
cache: "no-store",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
return response.ok ? url.toString() : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// This test skips all the initilization of the background script and just
|
// This test skips all the initialization of the background script and just
|
||||||
// focuses on making sure we don't accidently delete the initilization of
|
// focuses on making sure we don't accidentally delete the initialization of
|
||||||
// background vault syncing. This has happened before!
|
// background vault syncing. This has happened before!
|
||||||
describe("MainBackground sync task scheduling", () => {
|
describe("MainBackground sync task scheduling", () => {
|
||||||
it("includes code to schedule the sync interval task", () => {
|
it("includes code to schedule the sync interval task", () => {
|
||||||
|
|||||||
@@ -1230,6 +1230,9 @@ export default class MainBackground {
|
|||||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||||
this.logService,
|
this.logService,
|
||||||
this.notificationBackground,
|
this.notificationBackground,
|
||||||
|
this.taskService,
|
||||||
|
this.accountService,
|
||||||
|
this.cipherService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
|
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
|
||||||
|
|||||||
Reference in New Issue
Block a user