mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
Auth/pm 23620/auth request answering service (#15760)
* feat(notification-processing): [PM-19877] System Notification Implementation - Implemented auth request answering service. * test(notification-processing): [PM-19877] System Notification Implementation - Added tests.
This commit is contained in:
committed by
GitHub
parent
3b5342dfb3
commit
c828b3c4f4
@@ -14,6 +14,8 @@
|
|||||||
"build:watch:firefox": "npm run build:firefox -- --watch",
|
"build:watch:firefox": "npm run build:firefox -- --watch",
|
||||||
"build:watch:opera": "npm run build:opera -- --watch",
|
"build:watch:opera": "npm run build:opera -- --watch",
|
||||||
"build:watch:safari": "npm run build:safari -- --watch",
|
"build:watch:safari": "npm run build:safari -- --watch",
|
||||||
|
"build:watch:firefox:mv3": "cross-env MANIFEST_VERSION=3 npm run build:firefox -- --watch",
|
||||||
|
"build:watch:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run build:safari -- --watch",
|
||||||
"build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome",
|
"build:prod:chrome": "cross-env NODE_ENV=production npm run build:chrome",
|
||||||
"build:prod:edge": "cross-env NODE_ENV=production npm run build:edge",
|
"build:prod:edge": "cross-env NODE_ENV=production npm run build:edge",
|
||||||
"build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox",
|
"build:prod:firefox": "cross-env NODE_ENV=production npm run build:firefox",
|
||||||
|
|||||||
@@ -5107,6 +5107,18 @@
|
|||||||
"showNumberOfAutofillSuggestions": {
|
"showNumberOfAutofillSuggestions": {
|
||||||
"message": "Show number of login autofill suggestions on extension icon"
|
"message": "Show number of login autofill suggestions on extension icon"
|
||||||
},
|
},
|
||||||
|
"accountAccessRequested": {
|
||||||
|
"message": "Account access requested"
|
||||||
|
},
|
||||||
|
"confirmAccessAttempt": {
|
||||||
|
"message": "Confirm access attempt for $EMAIL$",
|
||||||
|
"placeholders": {
|
||||||
|
"email": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "name@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"showQuickCopyActions": {
|
"showQuickCopyActions": {
|
||||||
"message": "Show quick copy actions on Vault"
|
"message": "Show quick copy actions on Vault"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import "core-js/proposals/explicit-resource-management";
|
import "core-js/proposals/explicit-resource-management";
|
||||||
|
|
||||||
import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs";
|
import { filter, firstValueFrom, map, merge, Subject, switchMap, timeout } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +29,7 @@ import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/p
|
|||||||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
@@ -37,8 +38,10 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
|||||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
@@ -345,6 +348,7 @@ export default class MainBackground {
|
|||||||
serverNotificationsService: ServerNotificationsService;
|
serverNotificationsService: ServerNotificationsService;
|
||||||
systemNotificationService: SystemNotificationsService;
|
systemNotificationService: SystemNotificationsService;
|
||||||
actionsService: ActionsService;
|
actionsService: ActionsService;
|
||||||
|
authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction;
|
||||||
stateService: StateServiceAbstraction;
|
stateService: StateServiceAbstraction;
|
||||||
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||||
@@ -1107,13 +1111,22 @@ export default class MainBackground {
|
|||||||
|
|
||||||
if ("notifications" in chrome) {
|
if ("notifications" in chrome) {
|
||||||
this.systemNotificationService = new BrowserSystemNotificationService(
|
this.systemNotificationService = new BrowserSystemNotificationService(
|
||||||
this.logService,
|
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.systemNotificationService = new UnsupportedSystemNotificationsService();
|
this.systemNotificationService = new UnsupportedSystemNotificationsService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.authRequestAnsweringService = new AuthRequestAnsweringService(
|
||||||
|
this.accountService,
|
||||||
|
this.actionsService,
|
||||||
|
this.authService,
|
||||||
|
this.i18nService,
|
||||||
|
this.masterPasswordService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.systemNotificationService,
|
||||||
|
);
|
||||||
|
|
||||||
this.serverNotificationsService = new DefaultServerNotificationsService(
|
this.serverNotificationsService = new DefaultServerNotificationsService(
|
||||||
this.logService,
|
this.logService,
|
||||||
this.syncService,
|
this.syncService,
|
||||||
@@ -1125,6 +1138,7 @@ export default class MainBackground {
|
|||||||
new SignalRConnectionService(this.apiService, this.logService),
|
new SignalRConnectionService(this.apiService, this.logService),
|
||||||
this.authService,
|
this.authService,
|
||||||
this.webPushConnectionService,
|
this.webPushConnectionService,
|
||||||
|
this.authRequestAnsweringService,
|
||||||
this.configService,
|
this.configService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1376,6 +1390,10 @@ export default class MainBackground {
|
|||||||
if (this.webPushConnectionService instanceof WorkerWebPushConnectionService) {
|
if (this.webPushConnectionService instanceof WorkerWebPushConnectionService) {
|
||||||
this.webPushConnectionService.start();
|
this.webPushConnectionService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Putting this here so that all other services are initialized prior to trying to hook up
|
||||||
|
// subscriptions to the notification chrome events.
|
||||||
|
this.initNotificationSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrap() {
|
async bootstrap() {
|
||||||
@@ -1761,6 +1779,23 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is for creating any subscriptions for the background service worker. We do this
|
||||||
|
* here because it's important to run this during the evaluation period of the browser extension
|
||||||
|
* service worker.
|
||||||
|
*/
|
||||||
|
initNotificationSubscriptions() {
|
||||||
|
this.systemNotificationService.notificationClicked$
|
||||||
|
.pipe(
|
||||||
|
filter((n) => n.id.startsWith(AuthServerNotificationTags.AuthRequest + "_")),
|
||||||
|
map((n) => ({ event: n, authRequestId: n.id.split("_")[1] })),
|
||||||
|
switchMap(({ event }) =>
|
||||||
|
this.authRequestAnsweringService.handleAuthRequestNotificationClicked(event),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary solution to handle initialization of the overlay background behind a feature flag.
|
* Temporary solution to handle initialization of the overlay background behind a feature flag.
|
||||||
* Will be reverted to instantiation within the constructor once the feature flag is removed.
|
* Will be reverted to instantiation within the constructor once the feature flag is removed.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class BrowserActionsService implements ActionsService {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.logService.warning(
|
this.logService.warning(
|
||||||
`No openPopup function found on browser actions. On browser: ${deviceType} and manifest version: ${BrowserApi.manifestVersion}`,
|
`No openPopup function found on browser actions. On browser: ${DeviceType[deviceType]} and manifest version: ${BrowserApi.manifestVersion}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -36,7 +36,7 @@ export class BrowserActionsService implements ActionsService {
|
|||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
this.logService.warning(
|
this.logService.warning(
|
||||||
`Tried to open the popup from an unsupported device type: ${deviceType}`,
|
`Tried to open the popup from an unsupported device type: ${DeviceType[deviceType]}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import {
|
||||||
|
ButtonLocation,
|
||||||
|
SystemNotificationCreateInfo,
|
||||||
|
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
|
|
||||||
|
import { BrowserSystemNotificationService } from "./browser-system-notification.service";
|
||||||
|
|
||||||
|
type TestChromeEvent<T extends (...args: any[]) => any> = {
|
||||||
|
addListener: (callback: T) => void;
|
||||||
|
removeListener: (callback: T) => void;
|
||||||
|
// test-only helper
|
||||||
|
emit: (...args: Parameters<T>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestChromeEvent<T extends (...args: any[]) => any>(): TestChromeEvent<T> {
|
||||||
|
const listeners = new Set<T>();
|
||||||
|
return {
|
||||||
|
addListener: jest.fn((cb: T) => listeners.add(cb)),
|
||||||
|
removeListener: jest.fn((cb: T) => listeners.delete(cb)),
|
||||||
|
emit: (...args: Parameters<T>) => listeners.forEach((cb) => cb(...args)),
|
||||||
|
} as TestChromeEvent<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("BrowserSystemNotificationService", () => {
|
||||||
|
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
|
||||||
|
let service: BrowserSystemNotificationService;
|
||||||
|
|
||||||
|
let onButtonClicked: TestChromeEvent<(notificationId: string, buttonIndex: number) => void>;
|
||||||
|
let onClicked: TestChromeEvent<(notificationId: string) => void>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
onButtonClicked = createTestChromeEvent();
|
||||||
|
onClicked = createTestChromeEvent();
|
||||||
|
|
||||||
|
(global as any).chrome.notifications = {
|
||||||
|
onButtonClicked,
|
||||||
|
onClicked,
|
||||||
|
create: jest.fn((idOrOptions: any, optionsOrCallback: any, callback?: any) => {
|
||||||
|
if (typeof idOrOptions === "string") {
|
||||||
|
const cb = callback as (id: string) => void;
|
||||||
|
if (cb) {
|
||||||
|
cb(idOrOptions);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cb = optionsOrCallback as (id: string) => void;
|
||||||
|
if (cb) {
|
||||||
|
cb("generated-id");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
clear: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
platformUtilsService = {
|
||||||
|
getDevice: jest.fn().mockReturnValue(DeviceType.ChromeExtension),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
service = new BrowserSystemNotificationService(platformUtilsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isSupported", () => {
|
||||||
|
it("returns true when chrome.notifications exists", () => {
|
||||||
|
expect(service.isSupported()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when chrome.notifications is missing", () => {
|
||||||
|
const original = (global as any).chrome.notifications;
|
||||||
|
delete (global as any).chrome.notifications;
|
||||||
|
expect(service.isSupported()).toBe(false);
|
||||||
|
(global as any).chrome.notifications = original;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("passes id and options with buttons on non-Firefox", async () => {
|
||||||
|
const createInfo = {
|
||||||
|
id: "notif-1",
|
||||||
|
title: "Test Title",
|
||||||
|
body: "Body",
|
||||||
|
buttons: [{ title: "A" }, { title: "B" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
let capturedId: string | undefined;
|
||||||
|
let capturedOptions: any;
|
||||||
|
(chrome.notifications.create as jest.Mock).mockImplementationOnce(
|
||||||
|
(id: string, options: any, cb: (id: string) => void) => {
|
||||||
|
capturedId = id;
|
||||||
|
capturedOptions = options;
|
||||||
|
cb(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const id = await service.create(createInfo);
|
||||||
|
|
||||||
|
expect(id).toBe("notif-1");
|
||||||
|
expect(capturedId).toBe("notif-1");
|
||||||
|
expect(capturedOptions.title).toBe("Test Title");
|
||||||
|
expect(capturedOptions.message).toBe("Body");
|
||||||
|
expect(capturedOptions.type).toBe("basic");
|
||||||
|
expect(capturedOptions.iconUrl).toContain("images/icon128.png");
|
||||||
|
expect(capturedOptions.buttons).toEqual([{ title: "A" }, { title: "B" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits buttons on Firefox", async () => {
|
||||||
|
platformUtilsService.getDevice.mockReturnValue(DeviceType.FirefoxExtension);
|
||||||
|
|
||||||
|
const createInfo = {
|
||||||
|
id: "notif-2",
|
||||||
|
title: "Title",
|
||||||
|
body: "Body",
|
||||||
|
buttons: [{ title: "X" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
let capturedOptions: any;
|
||||||
|
(chrome.notifications.create as jest.Mock).mockImplementationOnce(
|
||||||
|
(_id: string, options: any, cb: (id: string) => void) => {
|
||||||
|
capturedOptions = options;
|
||||||
|
cb(_id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.create(createInfo);
|
||||||
|
|
||||||
|
expect("buttons" in capturedOptions).toBe(false);
|
||||||
|
expect(capturedOptions.title).toBe("Title");
|
||||||
|
expect(capturedOptions.message).toBe("Body");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports creating without an id", async () => {
|
||||||
|
const createInfo: SystemNotificationCreateInfo = {
|
||||||
|
title: "No Id",
|
||||||
|
body: "Body",
|
||||||
|
buttons: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let calledWithOptionsOnly = false;
|
||||||
|
(chrome.notifications.create as jest.Mock).mockImplementationOnce(
|
||||||
|
(options: any, cb: (id: string) => void) => {
|
||||||
|
calledWithOptionsOnly = typeof options === "object" && cb != null;
|
||||||
|
cb("generated-id");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const id = await service.create(createInfo);
|
||||||
|
expect(id).toBe("generated-id");
|
||||||
|
expect(calledWithOptionsOnly).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clear", () => {
|
||||||
|
it("invokes chrome.notifications.clear with the id", async () => {
|
||||||
|
await service.clear({ id: "to-clear" });
|
||||||
|
expect(chrome.notifications.clear).toHaveBeenCalledWith("to-clear");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("notificationClicked$", () => {
|
||||||
|
it("emits when a button is clicked", (done) => {
|
||||||
|
const expectEvent = {
|
||||||
|
id: "nid-1",
|
||||||
|
buttonIdentifier: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.notificationClicked$.subscribe((evt) => {
|
||||||
|
try {
|
||||||
|
expect(evt).toEqual(expectEvent);
|
||||||
|
done();
|
||||||
|
} catch (e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onButtonClicked.emit("nid-1", 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits when the notification itself is clicked", (done) => {
|
||||||
|
const expectEvent = {
|
||||||
|
id: "nid-2",
|
||||||
|
buttonIdentifier: ButtonLocation.NotificationButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
service.notificationClicked$.subscribe((evt) => {
|
||||||
|
try {
|
||||||
|
expect(evt).toEqual(expectEvent);
|
||||||
|
done();
|
||||||
|
} catch (e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onClicked.emit("nid-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { map, merge, Observable } from "rxjs";
|
import { map, merge, Observable } from "rxjs";
|
||||||
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
ButtonLocation,
|
ButtonLocation,
|
||||||
@@ -15,10 +15,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
|
|||||||
export class BrowserSystemNotificationService implements SystemNotificationsService {
|
export class BrowserSystemNotificationService implements SystemNotificationsService {
|
||||||
notificationClicked$: Observable<SystemNotificationEvent>;
|
notificationClicked$: Observable<SystemNotificationEvent>;
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly platformUtilsService: PlatformUtilsService) {
|
||||||
private logService: LogService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
) {
|
|
||||||
this.notificationClicked$ = merge(
|
this.notificationClicked$ = merge(
|
||||||
fromChromeEvent(chrome.notifications.onButtonClicked).pipe(
|
fromChromeEvent(chrome.notifications.onButtonClicked).pipe(
|
||||||
map(([notificationId, buttonIndex]) => ({
|
map(([notificationId, buttonIndex]) => ({
|
||||||
@@ -37,16 +34,28 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ
|
|||||||
|
|
||||||
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
|
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
|
||||||
return new Promise<string>((resolve) => {
|
return new Promise<string>((resolve) => {
|
||||||
chrome.notifications.create(
|
const deviceType: DeviceType = this.platformUtilsService.getDevice();
|
||||||
{
|
|
||||||
iconUrl: chrome.runtime.getURL("images/icon128.png"),
|
const options: chrome.notifications.NotificationOptions<true> = {
|
||||||
message: createInfo.body,
|
iconUrl: chrome.runtime.getURL("images/icon128.png"),
|
||||||
type: "basic",
|
message: createInfo.body,
|
||||||
title: createInfo.title,
|
type: "basic",
|
||||||
buttons: createInfo.buttons.map((value) => ({ title: value.title })),
|
title: createInfo.title,
|
||||||
},
|
buttons: createInfo.buttons.map((value) => ({ title: value.title })),
|
||||||
(notificationId) => resolve(notificationId),
|
};
|
||||||
);
|
|
||||||
|
// Firefox notification api does not support buttons.
|
||||||
|
if (deviceType === DeviceType.FirefoxExtension) {
|
||||||
|
delete options.buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createInfo.id != null) {
|
||||||
|
chrome.notifications.create(createInfo.id, options, (notificationId) =>
|
||||||
|
resolve(notificationId),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
chrome.notifications.create(options, (notificationId) => resolve(notificationId));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -556,6 +556,16 @@ const safeProviders: SafeProvider[] = [
|
|||||||
PlatformUtilsService,
|
PlatformUtilsService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ActionsService,
|
||||||
|
useClass: BrowserActionsService,
|
||||||
|
deps: [LogService, PlatformUtilsService],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SystemNotificationsService,
|
||||||
|
useClass: BrowserSystemNotificationService,
|
||||||
|
deps: [PlatformUtilsService],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: Fido2UserVerificationService,
|
provide: Fido2UserVerificationService,
|
||||||
useClass: Fido2UserVerificationService,
|
useClass: Fido2UserVerificationService,
|
||||||
@@ -588,7 +598,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: SystemNotificationsService,
|
provide: SystemNotificationsService,
|
||||||
useClass: BrowserSystemNotificationService,
|
useClass: BrowserSystemNotificationService,
|
||||||
deps: [LogService, PlatformUtilsService],
|
deps: [PlatformUtilsService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: LoginComponentService,
|
provide: LoginComponentService,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
InternalAccountService,
|
InternalAccountService,
|
||||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
@@ -104,6 +105,7 @@ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstract
|
|||||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||||
|
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
@@ -233,6 +235,8 @@ import { ValidationService } from "@bitwarden/common/platform/services/validatio
|
|||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||||
|
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications";
|
||||||
|
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
|
||||||
import {
|
import {
|
||||||
DefaultThemeStateService,
|
DefaultThemeStateService,
|
||||||
ThemeStateService,
|
ThemeStateService,
|
||||||
@@ -927,6 +931,29 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: UnsupportedActionsService,
|
useClass: UnsupportedActionsService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ActionsService,
|
||||||
|
useClass: UnsupportedActionsService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SystemNotificationsService,
|
||||||
|
useClass: UnsupportedSystemNotificationsService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: AuthRequestAnsweringServiceAbstraction,
|
||||||
|
useClass: AuthRequestAnsweringService,
|
||||||
|
deps: [
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
ActionsService,
|
||||||
|
AuthServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
MasterPasswordServiceAbstraction,
|
||||||
|
PlatformUtilsServiceAbstraction,
|
||||||
|
SystemNotificationsService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ServerNotificationsService,
|
provide: ServerNotificationsService,
|
||||||
useClass: devFlagEnabled("noopNotifications")
|
useClass: devFlagEnabled("noopNotifications")
|
||||||
@@ -943,6 +970,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
SignalRConnectionService,
|
SignalRConnectionService,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
WebPushConnectionService,
|
WebPushConnectionService,
|
||||||
|
AuthRequestAnsweringServiceAbstraction,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
export abstract class AuthRequestAnsweringServiceAbstraction {
|
||||||
|
abstract receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise<void>;
|
||||||
|
|
||||||
|
abstract handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const AuthServerNotificationTags = Object.freeze({
|
||||||
|
AuthRequest: "authRequest",
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||||
|
import {
|
||||||
|
ButtonLocation,
|
||||||
|
SystemNotificationEvent,
|
||||||
|
SystemNotificationsService,
|
||||||
|
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
|
||||||
|
|
||||||
|
describe("AuthRequestAnsweringService", () => {
|
||||||
|
let accountService: MockProxy<AccountService>;
|
||||||
|
let actionService: MockProxy<ActionsService>;
|
||||||
|
let authService: MockProxy<AuthService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let systemNotificationsService: MockProxy<SystemNotificationsService>;
|
||||||
|
|
||||||
|
let sut: AuthRequestAnsweringService;
|
||||||
|
|
||||||
|
const userId = "9f4c3452-6a45-48af-a7d0-74d3e8b65e4c" as UserId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
accountService = mock<AccountService>();
|
||||||
|
actionService = mock<ActionsService>();
|
||||||
|
authService = mock<AuthService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
masterPasswordService = { forceSetPasswordReason$: jest.fn() };
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
systemNotificationsService = mock<SystemNotificationsService>();
|
||||||
|
|
||||||
|
// Common defaults
|
||||||
|
authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
|
||||||
|
accountService.activeAccount$ = of({
|
||||||
|
id: userId,
|
||||||
|
email: "user@example.com",
|
||||||
|
emailVerified: true,
|
||||||
|
name: "User",
|
||||||
|
});
|
||||||
|
accountService.accounts$ = of({
|
||||||
|
[userId]: { email: "user@example.com", emailVerified: true, name: "User" },
|
||||||
|
});
|
||||||
|
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
|
||||||
|
of(ForceSetPasswordReason.None),
|
||||||
|
);
|
||||||
|
platformUtilsService.isPopupOpen.mockResolvedValue(false);
|
||||||
|
i18nService.t.mockImplementation(
|
||||||
|
(key: string, p1?: any) => `${key}${p1 != null ? ":" + p1 : ""}`,
|
||||||
|
);
|
||||||
|
systemNotificationsService.create.mockResolvedValue("notif-id");
|
||||||
|
|
||||||
|
sut = new AuthRequestAnsweringService(
|
||||||
|
accountService,
|
||||||
|
actionService,
|
||||||
|
authService,
|
||||||
|
i18nService,
|
||||||
|
masterPasswordService,
|
||||||
|
platformUtilsService,
|
||||||
|
systemNotificationsService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleAuthRequestNotificationClicked", () => {
|
||||||
|
it("clears notification and opens popup when notification body is clicked", async () => {
|
||||||
|
const event: SystemNotificationEvent = {
|
||||||
|
id: "123",
|
||||||
|
buttonIdentifier: ButtonLocation.NotificationButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sut.handleAuthRequestNotificationClicked(event);
|
||||||
|
|
||||||
|
expect(systemNotificationsService.clear).toHaveBeenCalledWith({ id: "123" });
|
||||||
|
expect(actionService.openPopup).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing when an optional button is clicked", async () => {
|
||||||
|
const event: SystemNotificationEvent = {
|
||||||
|
id: "123",
|
||||||
|
buttonIdentifier: ButtonLocation.FirstOptionalButton,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sut.handleAuthRequestNotificationClicked(event);
|
||||||
|
|
||||||
|
expect(systemNotificationsService.clear).not.toHaveBeenCalled();
|
||||||
|
expect(actionService.openPopup).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("receivedPendingAuthRequest", () => {
|
||||||
|
const authRequestId = "req-abc";
|
||||||
|
|
||||||
|
it("creates a system notification when popup is not open", async () => {
|
||||||
|
platformUtilsService.isPopupOpen.mockResolvedValue(false);
|
||||||
|
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
|
await sut.receivedPendingAuthRequest(userId, authRequestId);
|
||||||
|
|
||||||
|
expect(i18nService.t).toHaveBeenCalledWith("accountAccessRequested");
|
||||||
|
expect(i18nService.t).toHaveBeenCalledWith("confirmAccessAttempt", "user@example.com");
|
||||||
|
expect(systemNotificationsService.create).toHaveBeenCalledWith({
|
||||||
|
id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`,
|
||||||
|
title: "accountAccessRequested",
|
||||||
|
body: "confirmAccessAttempt:user@example.com",
|
||||||
|
buttons: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not create a notification when popup is open, user is active, unlocked, and no force set password", async () => {
|
||||||
|
platformUtilsService.isPopupOpen.mockResolvedValue(true);
|
||||||
|
authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked);
|
||||||
|
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
|
||||||
|
of(ForceSetPasswordReason.None),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.receivedPendingAuthRequest(userId, authRequestId);
|
||||||
|
|
||||||
|
expect(systemNotificationsService.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||||
|
import {
|
||||||
|
ButtonLocation,
|
||||||
|
SystemNotificationEvent,
|
||||||
|
SystemNotificationsService,
|
||||||
|
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
|
|
||||||
|
export class AuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction {
|
||||||
|
constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
|
private readonly actionService: ActionsService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
private readonly masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private readonly platformUtilsService: PlatformUtilsService,
|
||||||
|
private readonly systemNotificationsService: SystemNotificationsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void> {
|
||||||
|
if (event.buttonIdentifier === ButtonLocation.NotificationButton) {
|
||||||
|
await this.systemNotificationsService.clear({
|
||||||
|
id: `${event.id}`,
|
||||||
|
});
|
||||||
|
await this.actionService.openPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async receivedPendingAuthRequest(userId: UserId, authRequestId: string): Promise<void> {
|
||||||
|
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||||
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
|
const forceSetPasswordReason = await firstValueFrom(
|
||||||
|
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Is the popup already open?
|
||||||
|
if (
|
||||||
|
(await this.platformUtilsService.isPopupOpen()) &&
|
||||||
|
authStatus === AuthenticationStatus.Unlocked &&
|
||||||
|
activeUserId === userId &&
|
||||||
|
forceSetPasswordReason === ForceSetPasswordReason.None
|
||||||
|
) {
|
||||||
|
// TODO: Handled in 14934
|
||||||
|
} else {
|
||||||
|
// Get the user's email to include in the system notification
|
||||||
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||||
|
const emailForUser = accounts[userId].email;
|
||||||
|
|
||||||
|
await this.systemNotificationsService.create({
|
||||||
|
id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`,
|
||||||
|
title: this.i18nService.t("accountAccessRequested"),
|
||||||
|
body: this.i18nService.t("confirmAccessAttempt", emailForUser),
|
||||||
|
buttons: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
|
|
||||||
|
export class UnsupportedAuthRequestAnsweringService
|
||||||
|
implements AuthRequestAnsweringServiceAbstraction
|
||||||
|
{
|
||||||
|
constructor() {}
|
||||||
|
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void> {
|
||||||
|
throw new Error("Received pending auth request not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise<void> {
|
||||||
|
throw new Error("Received pending auth request not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subject } from "rxjs";
|
import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject } from "rxjs";
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason } from "@bitwarden/auth/common";
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
import { awaitAsync } from "../../../../spec";
|
import { awaitAsync } from "../../../../spec";
|
||||||
import { Matrix } from "../../../../spec/matrix";
|
import { Matrix } from "../../../../spec/matrix";
|
||||||
@@ -39,6 +41,7 @@ describe("NotificationsService", () => {
|
|||||||
let signalRNotificationConnectionService: MockProxy<SignalRConnectionService>;
|
let signalRNotificationConnectionService: MockProxy<SignalRConnectionService>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
|
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
|
||||||
|
let authRequestAnsweringService: MockProxy<AuthRequestAnsweringServiceAbstraction>;
|
||||||
let configService: MockProxy<ConfigService>;
|
let configService: MockProxy<ConfigService>;
|
||||||
|
|
||||||
let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
|
let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
|
||||||
@@ -66,9 +69,16 @@ describe("NotificationsService", () => {
|
|||||||
signalRNotificationConnectionService = mock<SignalRConnectionService>();
|
signalRNotificationConnectionService = mock<SignalRConnectionService>();
|
||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
webPushNotificationConnectionService = mock<WorkerWebPushConnectionService>();
|
webPushNotificationConnectionService = mock<WorkerWebPushConnectionService>();
|
||||||
|
authRequestAnsweringService = mock<AuthRequestAnsweringServiceAbstraction>();
|
||||||
configService = mock<ConfigService>();
|
configService = mock<ConfigService>();
|
||||||
|
|
||||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
// For these tests, use the active-user implementation (feature flag disabled)
|
||||||
|
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||||
|
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||||
|
[FeatureFlag.PushNotificationsWhenLocked]: true,
|
||||||
|
};
|
||||||
|
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||||
|
});
|
||||||
|
|
||||||
activeAccount = new BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>(null);
|
activeAccount = new BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>(null);
|
||||||
accountService.activeAccount$ = activeAccount.asObservable();
|
accountService.activeAccount$ = activeAccount.asObservable();
|
||||||
@@ -109,6 +119,7 @@ describe("NotificationsService", () => {
|
|||||||
signalRNotificationConnectionService,
|
signalRNotificationConnectionService,
|
||||||
authService,
|
authService,
|
||||||
webPushNotificationConnectionService,
|
webPushNotificationConnectionService,
|
||||||
|
authRequestAnsweringService,
|
||||||
configService,
|
configService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -116,7 +127,7 @@ describe("NotificationsService", () => {
|
|||||||
const mockUser1 = "user1" as UserId;
|
const mockUser1 = "user1" as UserId;
|
||||||
const mockUser2 = "user2" as UserId;
|
const mockUser2 = "user2" as UserId;
|
||||||
|
|
||||||
function emitActiveUser(userId: UserId) {
|
function emitActiveUser(userId: UserId | null) {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
activeAccount.next(null);
|
activeAccount.next(null);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
EMPTY,
|
EMPTY,
|
||||||
filter,
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason } from "@bitwarden/auth/common";
|
||||||
|
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
@@ -57,6 +59,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
|||||||
private readonly signalRConnectionService: SignalRConnectionService,
|
private readonly signalRConnectionService: SignalRConnectionService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly webPushConnectionService: WebPushConnectionService,
|
private readonly webPushConnectionService: WebPushConnectionService,
|
||||||
|
private readonly authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.notifications$ = this.accountService.activeAccount$.pipe(
|
this.notifications$ = this.accountService.activeAccount$.pipe(
|
||||||
@@ -227,8 +230,16 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
|||||||
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
||||||
break;
|
break;
|
||||||
case NotificationType.AuthRequest:
|
case NotificationType.AuthRequest:
|
||||||
// create notification
|
if (
|
||||||
|
await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await this.authRequestAnsweringService.receivedPendingAuthRequest(
|
||||||
|
notification.payload.userId,
|
||||||
|
notification.payload.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.messagingService.send("openLoginApproval", {
|
this.messagingService.send("openLoginApproval", {
|
||||||
notificationId: notification.payload.id,
|
notificationId: notification.payload.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
// This is currently tailored for chrome extension's api, if safari works
|
// This is currently tailored for Chrome extension's api, if Safari works
|
||||||
// differently where clicking a notification button produces a different
|
// differently where clicking a notification button produces a different
|
||||||
// identifier we need to reconcile that here.
|
// identifier we need to reconcile that here.
|
||||||
export const ButtonLocation = Object.freeze({
|
export const ButtonLocation = Object.freeze({
|
||||||
|
|||||||
Reference in New Issue
Block a user