1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00
Files
browser/libs/common/src/services/notifications.service.ts
Cesar Gonzalez 974162b1a4 [PM-6426] Create TaskSchedulerService and update long lived timeouts in the extension to leverage the new service (#8566)
* [PM-6426] Create TaskSchedulerService and update usage of long lived timeouts

* [PM-6426] Implementing nextSync timeout using TaskScheduler

* [PM-6426] Implementing systemClearClipboard using TaskScheduler

* [PM-6426] Fixing race condition with setting/unsetting active alarms

* [PM-6426] Implementing clear clipboard call on generatePasswordToClipboard with the TaskSchedulerService

* [PM-6426] Implementing abortTimeout for Fido2ClientService using TaskSchedulerService

* [PM-6426] Implementing reconnect timer timeout for NotificationService using the TaskSchedulerService

* [PM-6426] Implementing reconnect timer timeout for NotificationService using the TaskSchedulerService

* [PM-6426] Implementing sessionTimeout for LoginStrategyService using TaskSchedulerService

* [PM-6426] Implementing eventUploadInterval using TaskScheduler

* [PM-6426] Adding jest tests for the base TaskSchedulerService class

* [PM-6426] Updating jest tests for GeneratePasswordToClipboardCommand

* [PM-6426] Setting up the full sync process as an interval rather than a timeout

* [PM-6426] Renaming the scheduleNextSync alarm name

* [PM-6426] Fixing dependency references in services.module.ts

* [PM-6426] Adding jest tests for added BrowserApi methods

* [PM-6426] Refactoring small detail for how we identify the clear clipboard timeout in SystemService

* [PM-6426] Ensuring that we await clearing an established scheduled task for the notification service

* [PM-6426] Changing the name of the state definition for the TaskScheduler

* [PM-6426] Implementing jest tests for the BrowserTaskSchedulerService

* [PM-6426] Implementing jest tests for the BrowserTaskSchedulerService

* [PM-6426] Adding jest tests for the base TaskSchedulerService class

* [PM-6426] Finalizing jest tests for BrowserTaskScheduler class

* [PM-6426] Finalizing documentation on BrowserTaskSchedulerService

* [PM-6426] Fixing jest test for LoginStrategyService

* [PM-6426] Implementing compatibility for the browser.alarms api

* [PM-6426] Fixing how we check for the browser alarms api

* [PM-6426] Adding jest tests to the BrowserApi implementation

* [PM-6426] Aligning the implementation with our code guidelines for Angular components

* [PM-6426] Fixing jest tests and lint errors

* [PM-6426] Moving alarms api calls out of BrowserApi and structuring them within the BrowserTaskSchedulerService

* [PM-6426] Reworking implementation to register handlers separately from the call to those handlers

* [PM-6426] Adjusting how we register the fullSync scheduled task

* [PM-6426] Implementing approach for incorporating the user UUID when setting task handlers

* [PM-6426] Attempting to re-work implementation to facilitate userId-spcific alarms

* [PM-6426] Refactoring smaller details of the implementation

* [PM-6426] Working through the details of the implementation and setting up final refinments

* [PM-6426] Fixing some issues surrounding duplicate alarms triggering

* [PM-6426] Adjusting name for generate password to clipboard command task name

* [PM-6426] Fixing generate password to clipboard command jest tests

* [PM-6426] Working through jest tests and implementing a method to guard against setting a task without having a registered callback

* [PM-6426] Working through jest tests and implementing a method to guard against setting a task without having a registered callback

* [PM-6426] Implementing methodology for having a fallback to setTimeout if the browser context is lost in some manner

* [PM-6426] Working through jest tests

* [PM-6426] Working through jest tests

* [PM-6426] Working through jest tests

* [PM-6426] Working through jest tests

* [PM-6426] Finalizing stepped setInterval implementation

* [PM-6426] Implementing Jest tests for DefaultTaskSchedulerService

* [PM-6426] Adjusting jest tests

* [PM-6426] Adjusting jest tests

* [PM-6426] Adjusting jest tests

* [PM-6426] Fixing issues identified in code review

* [PM-6426] Fixing issues identified in code review

* [PM-6426] Removing user-based alarms and fixing an issue found with setting steppedd alarm interavals

* [PM-6426] Removing user-based alarms and fixing an issue found with setting steppedd alarm interavals

* [PM-6426] Fixing issue with typing information on a test

* [PM-6426] Using the getUpperBoundDelayInMinutes method to handle setting stepped alarms and setTimeout fallbacks

* [PM-6426] Removing the potential for the TaskScheduler to be optional

* [PM-6426] Reworking implementation to leverage subscription based deregistration of alarms

* [PM-6426] Fixing jest tests

* [PM-6426] Implementing foreground and background task scheduler services to avoid duplication of task scheudlers and to have the background setup as a fallback to the poopup tasks

* [PM-6426] Implementing foreground and background task scheduler services to avoid duplication of task scheudlers and to have the background setup as a fallback to the poopup tasks

* [PM-6426] Merging main into branch

* [PM-6426] Fixing issues with the CLI Service Container implementation

* [PM-6426] Reworking swallowed promises to contain a catch statement allow us to debug potential issues with registrations of alarms

* [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService

* [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService

* [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService

* [PM-6426] Adding jest tests to the ForegroundTaskSchedulerService and the BackgroundTaskSchedulerService

* [PM-6426] Adjusting implementation based on code review feedback

* [PM-6426] Reworking file structure

* [PM-6426] Reworking file structure

* [PM-6426] Adding comments to provide clarity on how the login strategy cache experiation state is used

* [PM-6426] Catching and logging erorrs that appear from methods that return a promise within VaultTimeoutService
2024-07-15 10:32:30 -05:00

264 lines
9.3 KiB
TypeScript

import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
import { firstValueFrom, Subscription } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { ApiService } from "../abstractions/api.service";
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
import { AuthService } from "../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../auth/enums/authentication-status";
import { NotificationType } from "../enums";
import {
NotificationResponse,
SyncCipherNotification,
SyncFolderNotification,
SyncSendNotification,
} from "../models/response/notification.response";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { EnvironmentService } from "../platform/abstractions/environment.service";
import { LogService } from "../platform/abstractions/log.service";
import { MessagingService } from "../platform/abstractions/messaging.service";
import { StateService } from "../platform/abstractions/state.service";
import { ScheduledTaskNames } from "../platform/scheduling/scheduled-task-name.enum";
import { TaskSchedulerService } from "../platform/scheduling/task-scheduler.service";
import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction";
export class NotificationsService implements NotificationsServiceAbstraction {
private signalrConnection: signalR.HubConnection;
private url: string;
private connected = false;
private inited = false;
private inactive = false;
private reconnectTimerSubscription: Subscription;
private isSyncingOnReconnect = true;
constructor(
private logService: LogService,
private syncService: SyncService,
private appIdService: AppIdService,
private apiService: ApiService,
private environmentService: EnvironmentService,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private stateService: StateService,
private authService: AuthService,
private messagingService: MessagingService,
private taskSchedulerService: TaskSchedulerService,
) {
this.taskSchedulerService.registerTaskHandler(
ScheduledTaskNames.notificationsReconnectTimeout,
() => this.reconnect(this.isSyncingOnReconnect),
);
this.environmentService.environment$.subscribe(() => {
if (!this.inited) {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.init();
});
}
async init(): Promise<void> {
this.inited = false;
this.url = (await firstValueFrom(this.environmentService.environment$)).getNotificationsUrl();
// Set notifications server URL to `https://-` to effectively disable communication
// with the notifications server from the client app
if (this.url === "https://-") {
return;
}
if (this.signalrConnection != null) {
this.signalrConnection.off("ReceiveMessage");
this.signalrConnection.off("Heartbeat");
await this.signalrConnection.stop();
this.connected = false;
this.signalrConnection = null;
}
this.signalrConnection = new signalR.HubConnectionBuilder()
.withUrl(this.url + "/hub", {
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol)
// .configureLogging(signalR.LogLevel.Trace)
.build();
this.signalrConnection.on("ReceiveMessage", (data: any) =>
this.processNotification(new NotificationResponse(data)),
);
// eslint-disable-next-line
this.signalrConnection.on("Heartbeat", (data: any) => {
/*console.log('Heartbeat!');*/
});
this.signalrConnection.onclose(() => {
this.connected = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.reconnect(true);
});
this.inited = true;
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(false);
}
}
async updateConnection(sync = false): Promise<void> {
if (!this.inited) {
return;
}
try {
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(sync);
} else {
await this.signalrConnection.stop();
}
} catch (e) {
this.logService.error(e.toString());
}
}
async reconnectFromActivity(): Promise<void> {
this.inactive = false;
if (this.inited && !this.connected) {
await this.reconnect(true);
}
}
async disconnectFromInactivity(): Promise<void> {
this.inactive = true;
if (this.inited && this.connected) {
await this.signalrConnection.stop();
}
}
private async processNotification(notification: NotificationResponse) {
const appId = await this.appIdService.getAppId();
if (notification == null || notification.contextId === appId) {
return;
}
const isAuthenticated = await this.stateService.getIsAuthenticated();
const payloadUserId = notification.payload.userId || notification.payload.UserId;
const myUserId = await this.stateService.getUserId();
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
return;
}
switch (notification.type) {
case NotificationType.SyncCipherCreate:
case NotificationType.SyncCipherUpdate:
await this.syncService.syncUpsertCipher(
notification.payload as SyncCipherNotification,
notification.type === NotificationType.SyncCipherUpdate,
);
break;
case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete:
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
break;
case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderUpdate:
await this.syncService.syncUpsertFolder(
notification.payload as SyncFolderNotification,
notification.type === NotificationType.SyncFolderUpdate,
);
break;
case NotificationType.SyncFolderDelete:
await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification);
break;
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
case NotificationType.SyncSettings:
if (isAuthenticated) {
await this.syncService.fullSync(false);
}
break;
case NotificationType.SyncOrganizations:
if (isAuthenticated) {
// An organization update may not have bumped the user's account revision date, so force a sync
await this.syncService.fullSync(true);
}
break;
case NotificationType.SyncOrgKeys:
if (isAuthenticated) {
await this.syncService.fullSync(true);
// Stop so a reconnect can be made
await this.signalrConnection.stop();
}
break;
case NotificationType.LogOut:
if (isAuthenticated) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.logoutCallback("logoutNotification");
}
break;
case NotificationType.SyncSendCreate:
case NotificationType.SyncSendUpdate:
await this.syncService.syncUpsertSend(
notification.payload as SyncSendNotification,
notification.type === NotificationType.SyncSendUpdate,
);
break;
case NotificationType.SyncSendDelete:
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
break;
case NotificationType.AuthRequest:
{
this.messagingService.send("openLoginApproval", {
notificationId: notification.payload.id,
});
}
break;
default:
break;
}
}
private async reconnect(sync: boolean) {
this.reconnectTimerSubscription?.unsubscribe();
if (this.connected || !this.inited || this.inactive) {
return;
}
const authedAndUnlocked = await this.isAuthedAndUnlocked();
if (!authedAndUnlocked) {
return;
}
try {
await this.signalrConnection.start();
this.connected = true;
if (sync) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
if (!this.connected) {
this.isSyncingOnReconnect = sync;
this.reconnectTimerSubscription = this.taskSchedulerService.setTimeout(
ScheduledTaskNames.notificationsReconnectTimeout,
this.random(120000, 300000),
);
}
}
private async isAuthedAndUnlocked() {
const authStatus = await this.authService.getAuthStatus();
return authStatus >= AuthenticationStatus.Unlocked;
}
private random(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}