mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-7978] Create ForegroundSyncService For Delegating fullSync Calls (#9192)
* Create ForegroundSyncService For Delegating `fullSync` calls to the background * Relax `isExternalMessage` to Allow For Typed Payload * Null Coalesce The `startListening` Method * Filter To Only External Messages
This commit is contained in:
@@ -230,6 +230,8 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils
|
|||||||
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
||||||
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
|
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
|
||||||
import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service";
|
import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service";
|
||||||
|
import { ForegroundSyncService } from "../platform/sync/foreground-sync.service";
|
||||||
|
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
|
||||||
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
|
||||||
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
|
||||||
import FilelessImporterBackground from "../tools/background/fileless-importer.background";
|
import FilelessImporterBackground from "../tools/background/fileless-importer.background";
|
||||||
@@ -339,6 +341,7 @@ export default class MainBackground {
|
|||||||
scriptInjectorService: BrowserScriptInjectorService;
|
scriptInjectorService: BrowserScriptInjectorService;
|
||||||
kdfConfigService: kdfConfigServiceAbstraction;
|
kdfConfigService: kdfConfigServiceAbstraction;
|
||||||
offscreenDocumentService: OffscreenDocumentService;
|
offscreenDocumentService: OffscreenDocumentService;
|
||||||
|
syncServiceListener: SyncServiceListener;
|
||||||
|
|
||||||
onUpdatedRan: boolean;
|
onUpdatedRan: boolean;
|
||||||
onReplacedRan: boolean;
|
onReplacedRan: boolean;
|
||||||
@@ -792,32 +795,52 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.providerService = new ProviderService(this.stateProvider);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
|
|
||||||
this.syncService = new SyncService(
|
if (this.popupOnlyContext) {
|
||||||
this.masterPasswordService,
|
this.syncService = new ForegroundSyncService(
|
||||||
this.accountService,
|
this.stateService,
|
||||||
this.apiService,
|
this.folderService,
|
||||||
this.domainSettingsService,
|
this.folderApiService,
|
||||||
this.folderService,
|
this.messagingService,
|
||||||
this.cipherService,
|
this.logService,
|
||||||
this.cryptoService,
|
this.cipherService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.messagingService,
|
this.apiService,
|
||||||
this.policyService,
|
this.accountService,
|
||||||
this.sendService,
|
this.authService,
|
||||||
this.logService,
|
this.sendService,
|
||||||
this.keyConnectorService,
|
this.sendApiService,
|
||||||
this.stateService,
|
messageListener,
|
||||||
this.providerService,
|
);
|
||||||
this.folderApiService,
|
} else {
|
||||||
this.organizationService,
|
this.syncService = new SyncService(
|
||||||
this.sendApiService,
|
this.masterPasswordService,
|
||||||
this.userDecryptionOptionsService,
|
this.accountService,
|
||||||
this.avatarService,
|
this.apiService,
|
||||||
logoutCallback,
|
this.domainSettingsService,
|
||||||
this.billingAccountProfileStateService,
|
this.folderService,
|
||||||
this.tokenService,
|
this.cipherService,
|
||||||
this.authService,
|
this.cryptoService,
|
||||||
);
|
this.collectionService,
|
||||||
|
this.messagingService,
|
||||||
|
this.policyService,
|
||||||
|
this.sendService,
|
||||||
|
this.logService,
|
||||||
|
this.keyConnectorService,
|
||||||
|
this.stateService,
|
||||||
|
this.providerService,
|
||||||
|
this.folderApiService,
|
||||||
|
this.organizationService,
|
||||||
|
this.sendApiService,
|
||||||
|
this.userDecryptionOptionsService,
|
||||||
|
this.avatarService,
|
||||||
|
logoutCallback,
|
||||||
|
this.billingAccountProfileStateService,
|
||||||
|
this.tokenService,
|
||||||
|
this.authService,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener);
|
||||||
|
}
|
||||||
this.eventUploadService = new EventUploadService(
|
this.eventUploadService = new EventUploadService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
@@ -1141,6 +1164,7 @@ export default class MainBackground {
|
|||||||
this.contextMenusBackground?.init();
|
this.contextMenusBackground?.init();
|
||||||
await this.idleBackground.init();
|
await this.idleBackground.init();
|
||||||
this.webRequestBackground?.startListening();
|
this.webRequestBackground?.startListening();
|
||||||
|
this.syncServiceListener?.startListening();
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|||||||
79
apps/browser/src/platform/sync/foreground-sync.service.ts
Normal file
79
apps/browser/src/platform/sync/foreground-sync.service.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { firstValueFrom, timeout } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import {
|
||||||
|
CommandDefinition,
|
||||||
|
MessageListener,
|
||||||
|
MessageSender,
|
||||||
|
} from "@bitwarden/common/platform/messaging";
|
||||||
|
import { CoreSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||||
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
|
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
|
|
||||||
|
const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted");
|
||||||
|
export const DO_FULL_SYNC = new CommandDefinition<{
|
||||||
|
forceSync: boolean;
|
||||||
|
allowThrowOnError: boolean;
|
||||||
|
}>("doFullSync");
|
||||||
|
|
||||||
|
export class ForegroundSyncService extends CoreSyncService {
|
||||||
|
constructor(
|
||||||
|
stateService: StateService,
|
||||||
|
folderService: InternalFolderService,
|
||||||
|
folderApiService: FolderApiServiceAbstraction,
|
||||||
|
messageSender: MessageSender,
|
||||||
|
logService: LogService,
|
||||||
|
cipherService: CipherService,
|
||||||
|
collectionService: CollectionService,
|
||||||
|
apiService: ApiService,
|
||||||
|
accountService: AccountService,
|
||||||
|
authService: AuthService,
|
||||||
|
sendService: InternalSendService,
|
||||||
|
sendApiService: SendApiService,
|
||||||
|
private readonly messageListener: MessageListener,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
stateService,
|
||||||
|
folderService,
|
||||||
|
folderApiService,
|
||||||
|
messageSender,
|
||||||
|
logService,
|
||||||
|
cipherService,
|
||||||
|
collectionService,
|
||||||
|
apiService,
|
||||||
|
accountService,
|
||||||
|
authService,
|
||||||
|
sendService,
|
||||||
|
sendApiService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise<boolean> {
|
||||||
|
this.syncInProgress = true;
|
||||||
|
try {
|
||||||
|
const syncCompletedPromise = firstValueFrom(
|
||||||
|
this.messageListener.messages$(SYNC_COMPLETED).pipe(
|
||||||
|
timeout({
|
||||||
|
first: 10_000,
|
||||||
|
with: () => {
|
||||||
|
throw new Error("Timeout while doing a fullSync call.");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError });
|
||||||
|
const result = await syncCompletedPromise;
|
||||||
|
return result.successfully;
|
||||||
|
} finally {
|
||||||
|
this.syncInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
apps/browser/src/platform/sync/sync-service.listener.ts
Normal file
25
apps/browser/src/platform/sync/sync-service.listener.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Subscription, concatMap, filter } from "rxjs";
|
||||||
|
|
||||||
|
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
import { DO_FULL_SYNC } from "./foreground-sync.service";
|
||||||
|
|
||||||
|
export class SyncServiceListener {
|
||||||
|
constructor(
|
||||||
|
private readonly syncService: SyncService,
|
||||||
|
private readonly messageListener: MessageListener,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
startListening(): Subscription {
|
||||||
|
return this.messageListener
|
||||||
|
.messages$(DO_FULL_SYNC)
|
||||||
|
.pipe(
|
||||||
|
filter((message) => isExternalMessage(message)),
|
||||||
|
concatMap(async ({ forceSync, allowThrowOnError }) => {
|
||||||
|
await this.syncService.fullSync(forceSync, allowThrowOnError);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ export const getCommand = (commandDefinition: CommandDefinition<object> | string
|
|||||||
|
|
||||||
export const EXTERNAL_SOURCE_TAG = Symbol("externalSource");
|
export const EXTERNAL_SOURCE_TAG = Symbol("externalSource");
|
||||||
|
|
||||||
export const isExternalMessage = (message: Message<object>) => {
|
export const isExternalMessage = (message: Record<PropertyKey, unknown>) => {
|
||||||
return (message as Record<PropertyKey, unknown>)?.[EXTERNAL_SOURCE_TAG] === true;
|
return message?.[EXTERNAL_SOURCE_TAG] === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map(
|
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map(
|
||||||
|
|||||||
230
libs/common/src/platform/sync/core-sync.service.ts
Normal file
230
libs/common/src/platform/sync/core-sync.service.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import { firstValueFrom, map, of, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
|
import {
|
||||||
|
SyncCipherNotification,
|
||||||
|
SyncFolderNotification,
|
||||||
|
SyncSendNotification,
|
||||||
|
} from "../../models/response/notification.response";
|
||||||
|
import { SendData } from "../../tools/send/models/data/send.data";
|
||||||
|
import { SendApiService } from "../../tools/send/services/send-api.service.abstraction";
|
||||||
|
import { InternalSendService } from "../../tools/send/services/send.service.abstraction";
|
||||||
|
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "../../vault/abstractions/collection.service";
|
||||||
|
import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
|
import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
||||||
|
import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
|
import { FolderData } from "../../vault/models/data/folder.data";
|
||||||
|
import { LogService } from "../abstractions/log.service";
|
||||||
|
import { StateService } from "../abstractions/state.service";
|
||||||
|
import { MessageSender } from "../messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core SyncService Logic EXCEPT for fullSync so that implementations can differ.
|
||||||
|
*/
|
||||||
|
export abstract class CoreSyncService implements SyncService {
|
||||||
|
syncInProgress = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly stateService: StateService,
|
||||||
|
protected readonly folderService: InternalFolderService,
|
||||||
|
protected readonly folderApiService: FolderApiServiceAbstraction,
|
||||||
|
protected readonly messageSender: MessageSender,
|
||||||
|
protected readonly logService: LogService,
|
||||||
|
protected readonly cipherService: CipherService,
|
||||||
|
protected readonly collectionService: CollectionService,
|
||||||
|
protected readonly apiService: ApiService,
|
||||||
|
protected readonly accountService: AccountService,
|
||||||
|
protected readonly authService: AuthService,
|
||||||
|
protected readonly sendService: InternalSendService,
|
||||||
|
protected readonly sendApiService: SendApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise<boolean>;
|
||||||
|
|
||||||
|
async getLastSync(): Promise<Date> {
|
||||||
|
if ((await this.stateService.getUserId()) == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSync = await this.stateService.getLastSync();
|
||||||
|
if (lastSync) {
|
||||||
|
return new Date(lastSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setLastSync(date: Date, userId?: string): Promise<any> {
|
||||||
|
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
if (await this.stateService.getIsAuthenticated()) {
|
||||||
|
try {
|
||||||
|
const localFolder = await this.folderService.get(notification.id);
|
||||||
|
if (
|
||||||
|
(!isEdit && localFolder == null) ||
|
||||||
|
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)
|
||||||
|
) {
|
||||||
|
const remoteFolder = await this.folderApiService.get(notification.id);
|
||||||
|
if (remoteFolder != null) {
|
||||||
|
await this.folderService.upsert(new FolderData(remoteFolder));
|
||||||
|
this.messageSender.send("syncedUpsertedFolder", { folderId: notification.id });
|
||||||
|
return this.syncCompleted(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
if (await this.stateService.getIsAuthenticated()) {
|
||||||
|
await this.folderService.delete(notification.id);
|
||||||
|
this.messageSender.send("syncedDeletedFolder", { folderId: notification.id });
|
||||||
|
this.syncCompleted(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
if (await this.stateService.getIsAuthenticated()) {
|
||||||
|
try {
|
||||||
|
let shouldUpdate = true;
|
||||||
|
const localCipher = await this.cipherService.get(notification.id);
|
||||||
|
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
|
||||||
|
shouldUpdate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkCollections = false;
|
||||||
|
if (shouldUpdate) {
|
||||||
|
if (isEdit) {
|
||||||
|
shouldUpdate = localCipher != null;
|
||||||
|
checkCollections = true;
|
||||||
|
} else {
|
||||||
|
if (notification.collectionIds == null || notification.organizationId == null) {
|
||||||
|
shouldUpdate = localCipher == null;
|
||||||
|
} else {
|
||||||
|
shouldUpdate = false;
|
||||||
|
checkCollections = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!shouldUpdate &&
|
||||||
|
checkCollections &&
|
||||||
|
notification.organizationId != null &&
|
||||||
|
notification.collectionIds != null &&
|
||||||
|
notification.collectionIds.length > 0
|
||||||
|
) {
|
||||||
|
const collections = await this.collectionService.getAll();
|
||||||
|
if (collections != null) {
|
||||||
|
for (let i = 0; i < collections.length; i++) {
|
||||||
|
if (notification.collectionIds.indexOf(collections[i].id) > -1) {
|
||||||
|
shouldUpdate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
const remoteCipher = await this.apiService.getFullCipherDetails(notification.id);
|
||||||
|
if (remoteCipher != null) {
|
||||||
|
await this.cipherService.upsert(new CipherData(remoteCipher));
|
||||||
|
this.messageSender.send("syncedUpsertedCipher", { cipherId: notification.id });
|
||||||
|
return this.syncCompleted(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e != null && e.statusCode === 404 && isEdit) {
|
||||||
|
await this.cipherService.delete(notification.id);
|
||||||
|
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
|
||||||
|
return this.syncCompleted(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
if (await this.stateService.getIsAuthenticated()) {
|
||||||
|
await this.cipherService.delete(notification.id);
|
||||||
|
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
|
||||||
|
return this.syncCompleted(true);
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
const [activeUserId, status] = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(
|
||||||
|
switchMap((a) => {
|
||||||
|
if (a == null) {
|
||||||
|
of([null, AuthenticationStatus.LoggedOut]);
|
||||||
|
}
|
||||||
|
return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s]));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Process only notifications for currently active user when user is not logged out
|
||||||
|
// TODO: once send service allows data manipulation of non-active users, this should process any received notification
|
||||||
|
if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) {
|
||||||
|
try {
|
||||||
|
const localSend = await firstValueFrom(this.sendService.get$(notification.id));
|
||||||
|
if (
|
||||||
|
(!isEdit && localSend == null) ||
|
||||||
|
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)
|
||||||
|
) {
|
||||||
|
const remoteSend = await this.sendApiService.getSend(notification.id);
|
||||||
|
if (remoteSend != null) {
|
||||||
|
await this.sendService.upsert(new SendData(remoteSend));
|
||||||
|
this.messageSender.send("syncedUpsertedSend", { sendId: notification.id });
|
||||||
|
return this.syncCompleted(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
|
||||||
|
this.syncStarted();
|
||||||
|
if (await this.stateService.getIsAuthenticated()) {
|
||||||
|
await this.sendService.delete(notification.id);
|
||||||
|
this.messageSender.send("syncedDeletedSend", { sendId: notification.id });
|
||||||
|
this.syncCompleted(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.syncCompleted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
protected syncStarted() {
|
||||||
|
this.syncInProgress = true;
|
||||||
|
this.messageSender.send("syncStarted");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected syncCompleted(successfully: boolean): boolean {
|
||||||
|
this.syncInProgress = false;
|
||||||
|
this.messageSender.send("syncCompleted", { successfully: successfully });
|
||||||
|
return successfully;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/common/src/platform/sync/internal.ts
Normal file
1
libs/common/src/platform/sync/internal.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { CoreSyncService } from "./core-sync.service";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { firstValueFrom, map, of, switchMap } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
|
||||||
@@ -17,22 +17,17 @@ import { AvatarService } from "../../../auth/abstractions/avatar.service";
|
|||||||
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
|
||||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
|
||||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||||
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
|
||||||
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||||
import {
|
|
||||||
SyncCipherNotification,
|
|
||||||
SyncFolderNotification,
|
|
||||||
SyncSendNotification,
|
|
||||||
} from "../../../models/response/notification.response";
|
|
||||||
import { ProfileResponse } from "../../../models/response/profile.response";
|
import { ProfileResponse } from "../../../models/response/profile.response";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { LogService } from "../../../platform/abstractions/log.service";
|
import { LogService } from "../../../platform/abstractions/log.service";
|
||||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
|
||||||
import { StateService } from "../../../platform/abstractions/state.service";
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
|
import { MessageSender } from "../../../platform/messaging";
|
||||||
import { sequentialize } from "../../../platform/misc/sequentialize";
|
import { sequentialize } from "../../../platform/misc/sequentialize";
|
||||||
|
import { CoreSyncService } from "../../../platform/sync/core-sync.service";
|
||||||
import { SendData } from "../../../tools/send/models/data/send.data";
|
import { SendData } from "../../../tools/send/models/data/send.data";
|
||||||
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
||||||
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
|
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
|
||||||
@@ -40,7 +35,6 @@ import { InternalSendService } from "../../../tools/send/services/send.service.a
|
|||||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||||
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
|
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { SyncService as SyncServiceAbstraction } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
|
||||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||||
@@ -49,55 +43,51 @@ import { CollectionService } from "../../abstractions/collection.service";
|
|||||||
import { CollectionData } from "../../models/data/collection.data";
|
import { CollectionData } from "../../models/data/collection.data";
|
||||||
import { CollectionDetailsResponse } from "../../models/response/collection.response";
|
import { CollectionDetailsResponse } from "../../models/response/collection.response";
|
||||||
|
|
||||||
export class SyncService implements SyncServiceAbstraction {
|
export class SyncService extends CoreSyncService {
|
||||||
syncInProgress = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
private accountService: AccountService,
|
accountService: AccountService,
|
||||||
private apiService: ApiService,
|
apiService: ApiService,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private folderService: InternalFolderService,
|
folderService: InternalFolderService,
|
||||||
private cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private collectionService: CollectionService,
|
collectionService: CollectionService,
|
||||||
private messagingService: MessagingService,
|
messageSender: MessageSender,
|
||||||
private policyService: InternalPolicyService,
|
private policyService: InternalPolicyService,
|
||||||
private sendService: InternalSendService,
|
sendService: InternalSendService,
|
||||||
private logService: LogService,
|
logService: LogService,
|
||||||
private keyConnectorService: KeyConnectorService,
|
private keyConnectorService: KeyConnectorService,
|
||||||
private stateService: StateService,
|
stateService: StateService,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
private folderApiService: FolderApiServiceAbstraction,
|
folderApiService: FolderApiServiceAbstraction,
|
||||||
private organizationService: InternalOrganizationServiceAbstraction,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
private sendApiService: SendApiService,
|
sendApiService: SendApiService,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private avatarService: AvatarService,
|
private avatarService: AvatarService,
|
||||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private authService: AuthService,
|
authService: AuthService,
|
||||||
) {}
|
) {
|
||||||
|
super(
|
||||||
async getLastSync(): Promise<Date> {
|
stateService,
|
||||||
if ((await this.stateService.getUserId()) == null) {
|
folderService,
|
||||||
return null;
|
folderApiService,
|
||||||
}
|
messageSender,
|
||||||
|
logService,
|
||||||
const lastSync = await this.stateService.getLastSync();
|
cipherService,
|
||||||
if (lastSync) {
|
collectionService,
|
||||||
return new Date(lastSync);
|
apiService,
|
||||||
}
|
accountService,
|
||||||
|
authService,
|
||||||
return null;
|
sendService,
|
||||||
}
|
sendApiService,
|
||||||
|
);
|
||||||
async setLastSync(date: Date, userId?: string): Promise<any> {
|
|
||||||
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@sequentialize(() => "fullSync")
|
@sequentialize(() => "fullSync")
|
||||||
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
override async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
||||||
this.syncStarted();
|
this.syncStarted();
|
||||||
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
@@ -110,6 +100,7 @@ export class SyncService implements SyncServiceAbstraction {
|
|||||||
needsSync = await this.needsSyncing(forceSync);
|
needsSync = await this.needsSyncing(forceSync);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (allowThrowOnError) {
|
if (allowThrowOnError) {
|
||||||
|
this.syncCompleted(false);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +126,7 @@ export class SyncService implements SyncServiceAbstraction {
|
|||||||
return this.syncCompleted(true);
|
return this.syncCompleted(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (allowThrowOnError) {
|
if (allowThrowOnError) {
|
||||||
|
this.syncCompleted(false);
|
||||||
throw e;
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
return this.syncCompleted(false);
|
return this.syncCompleted(false);
|
||||||
@@ -142,171 +134,6 @@ export class SyncService implements SyncServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
if (await this.stateService.getIsAuthenticated()) {
|
|
||||||
try {
|
|
||||||
const localFolder = await this.folderService.get(notification.id);
|
|
||||||
if (
|
|
||||||
(!isEdit && localFolder == null) ||
|
|
||||||
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)
|
|
||||||
) {
|
|
||||||
const remoteFolder = await this.folderApiService.get(notification.id);
|
|
||||||
if (remoteFolder != null) {
|
|
||||||
await this.folderService.upsert(new FolderData(remoteFolder));
|
|
||||||
this.messagingService.send("syncedUpsertedFolder", { folderId: notification.id });
|
|
||||||
return this.syncCompleted(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
if (await this.stateService.getIsAuthenticated()) {
|
|
||||||
await this.folderService.delete(notification.id);
|
|
||||||
this.messagingService.send("syncedDeletedFolder", { folderId: notification.id });
|
|
||||||
this.syncCompleted(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
if (await this.stateService.getIsAuthenticated()) {
|
|
||||||
try {
|
|
||||||
let shouldUpdate = true;
|
|
||||||
const localCipher = await this.cipherService.get(notification.id);
|
|
||||||
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
|
|
||||||
shouldUpdate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let checkCollections = false;
|
|
||||||
if (shouldUpdate) {
|
|
||||||
if (isEdit) {
|
|
||||||
shouldUpdate = localCipher != null;
|
|
||||||
checkCollections = true;
|
|
||||||
} else {
|
|
||||||
if (notification.collectionIds == null || notification.organizationId == null) {
|
|
||||||
shouldUpdate = localCipher == null;
|
|
||||||
} else {
|
|
||||||
shouldUpdate = false;
|
|
||||||
checkCollections = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!shouldUpdate &&
|
|
||||||
checkCollections &&
|
|
||||||
notification.organizationId != null &&
|
|
||||||
notification.collectionIds != null &&
|
|
||||||
notification.collectionIds.length > 0
|
|
||||||
) {
|
|
||||||
const collections = await this.collectionService.getAll();
|
|
||||||
if (collections != null) {
|
|
||||||
for (let i = 0; i < collections.length; i++) {
|
|
||||||
if (notification.collectionIds.indexOf(collections[i].id) > -1) {
|
|
||||||
shouldUpdate = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
|
||||||
const remoteCipher = await this.apiService.getFullCipherDetails(notification.id);
|
|
||||||
if (remoteCipher != null) {
|
|
||||||
await this.cipherService.upsert(new CipherData(remoteCipher));
|
|
||||||
this.messagingService.send("syncedUpsertedCipher", { cipherId: notification.id });
|
|
||||||
return this.syncCompleted(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e != null && e.statusCode === 404 && isEdit) {
|
|
||||||
await this.cipherService.delete(notification.id);
|
|
||||||
this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id });
|
|
||||||
return this.syncCompleted(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
if (await this.stateService.getIsAuthenticated()) {
|
|
||||||
await this.cipherService.delete(notification.id);
|
|
||||||
this.messagingService.send("syncedDeletedCipher", { cipherId: notification.id });
|
|
||||||
return this.syncCompleted(true);
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
const [activeUserId, status] = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(
|
|
||||||
switchMap((a) => {
|
|
||||||
if (a == null) {
|
|
||||||
of([null, AuthenticationStatus.LoggedOut]);
|
|
||||||
}
|
|
||||||
return this.authService.authStatusFor$(a.id).pipe(map((s) => [a.id, s]));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Process only notifications for currently active user when user is not logged out
|
|
||||||
// TODO: once send service allows data manipulation of non-active users, this should process any received notification
|
|
||||||
if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) {
|
|
||||||
try {
|
|
||||||
const localSend = await firstValueFrom(this.sendService.get$(notification.id));
|
|
||||||
if (
|
|
||||||
(!isEdit && localSend == null) ||
|
|
||||||
(isEdit && localSend != null && localSend.revisionDate < notification.revisionDate)
|
|
||||||
) {
|
|
||||||
const remoteSend = await this.sendApiService.getSend(notification.id);
|
|
||||||
if (remoteSend != null) {
|
|
||||||
await this.sendService.upsert(new SendData(remoteSend));
|
|
||||||
this.messagingService.send("syncedUpsertedSend", { sendId: notification.id });
|
|
||||||
return this.syncCompleted(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncDeleteSend(notification: SyncSendNotification): Promise<boolean> {
|
|
||||||
this.syncStarted();
|
|
||||||
if (await this.stateService.getIsAuthenticated()) {
|
|
||||||
await this.sendService.delete(notification.id);
|
|
||||||
this.messagingService.send("syncedDeletedSend", { sendId: notification.id });
|
|
||||||
this.syncCompleted(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return this.syncCompleted(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
private syncStarted() {
|
|
||||||
this.syncInProgress = true;
|
|
||||||
this.messagingService.send("syncStarted");
|
|
||||||
}
|
|
||||||
|
|
||||||
private syncCompleted(successfully: boolean): boolean {
|
|
||||||
this.syncInProgress = false;
|
|
||||||
this.messagingService.send("syncCompleted", { successfully: successfully });
|
|
||||||
return successfully;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async needsSyncing(forceSync: boolean) {
|
private async needsSyncing(forceSync: boolean) {
|
||||||
if (forceSync) {
|
if (forceSync) {
|
||||||
return true;
|
return true;
|
||||||
@@ -365,7 +192,7 @@ export class SyncService implements SyncServiceAbstraction {
|
|||||||
|
|
||||||
if (await this.keyConnectorService.userNeedsMigration()) {
|
if (await this.keyConnectorService.userNeedsMigration()) {
|
||||||
await this.keyConnectorService.setConvertAccountRequired(true);
|
await this.keyConnectorService.setConvertAccountRequired(true);
|
||||||
this.messagingService.send("convertAccountToKeyConnector");
|
this.messageSender.send("convertAccountToKeyConnector");
|
||||||
} else {
|
} else {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|||||||
Reference in New Issue
Block a user