mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PS-1092] Organization Service Observables (#3462)
* Update imports * Implement observables in a few places * Add tests * Get all clients working * Use _destroy * Address PR feedback * Address PR feedback * Address feedback
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { Organization } from "../models/domain/organization";
|
||||
|
||||
export abstract class OrganizationService {
|
||||
get: (id: string) => Promise<Organization>;
|
||||
getByIdentifier: (identifier: string) => Promise<Organization>;
|
||||
getAll: (userId?: string) => Promise<Organization[]>;
|
||||
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
|
||||
canManageSponsorships: () => Promise<boolean>;
|
||||
hasOrganizations: (userId?: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { I18nService } from "../i18n.service";
|
||||
|
||||
export function canAccessToolsTab(org: Organization): boolean {
|
||||
return org.canAccessImportExport || org.canAccessReports;
|
||||
}
|
||||
|
||||
export function canAccessSettingsTab(org: Organization): boolean {
|
||||
return org.isOwner;
|
||||
}
|
||||
|
||||
export function canAccessManageTab(org: Organization): boolean {
|
||||
return (
|
||||
org.canCreateNewCollections ||
|
||||
org.canEditAnyCollection ||
|
||||
org.canDeleteAnyCollection ||
|
||||
org.canEditAssignedCollections ||
|
||||
org.canDeleteAssignedCollections ||
|
||||
org.canAccessEventLogs ||
|
||||
org.canManageGroups ||
|
||||
org.canManageUsers ||
|
||||
org.canManagePolicies ||
|
||||
org.canManageSso ||
|
||||
org.canManageScim
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
return canAccessToolsTab(org) || canAccessSettingsTab(org) || canAccessManageTab(org);
|
||||
}
|
||||
|
||||
export function getOrganizationById(id: string) {
|
||||
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
|
||||
}
|
||||
|
||||
export function canAccessAdmin(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name"))
|
||||
);
|
||||
}
|
||||
|
||||
export abstract class OrganizationService {
|
||||
organizations$: Observable<Organization[]>;
|
||||
|
||||
get: (id: string) => Organization;
|
||||
getByIdentifier: (identifier: string) => Organization;
|
||||
getAll: (userId?: string) => Promise<Organization[]>;
|
||||
canManageSponsorships: () => Promise<boolean>;
|
||||
hasOrganizations: () => boolean;
|
||||
}
|
||||
@@ -273,7 +273,13 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use OrganizationService
|
||||
*/
|
||||
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use OrganizationService
|
||||
*/
|
||||
setOrganizations: (
|
||||
value: { [id: string]: OrganizationData },
|
||||
options?: StorageOptions
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
SyncCipherNotification,
|
||||
SyncFolderNotification,
|
||||
SyncSendNotification,
|
||||
} from "../../models/response/notificationResponse";
|
||||
import { SyncEventArgs } from "../../types/syncEventArgs";
|
||||
|
||||
export abstract class SyncService {
|
||||
syncInProgress: boolean;
|
||||
|
||||
sync$: Observable<SyncEventArgs>;
|
||||
|
||||
getLastSync: () => Promise<Date>;
|
||||
setLastSync: (date: Date, userId?: string) => Promise<any>;
|
||||
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SyncEventArgs } from "../../types/syncEventArgs";
|
||||
|
||||
export abstract class SyncNotifierService {
|
||||
sync$: Observable<SyncEventArgs>;
|
||||
next: (event: SyncEventArgs) => void;
|
||||
}
|
||||
@@ -310,8 +310,11 @@ export class Utils {
|
||||
return map;
|
||||
}
|
||||
|
||||
static getSortFunction(i18nService: I18nService, prop: string) {
|
||||
return (a: any, b: any) => {
|
||||
static getSortFunction<T>(
|
||||
i18nService: I18nService,
|
||||
prop: { [K in keyof T]: T[K] extends string ? K : never }[keyof T]
|
||||
): (a: T, b: T) => number {
|
||||
return (a, b) => {
|
||||
if (a[prop] == null && b[prop] != null) {
|
||||
return -1;
|
||||
}
|
||||
@@ -322,9 +325,10 @@ export class Utils {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The `as unknown as string` here is unfortunate because typescript doesn't property understand that the return of T[prop] will be a string
|
||||
return i18nService.collator
|
||||
? i18nService.collator.compare(a[prop], b[prop])
|
||||
: a[prop].localeCompare(b[prop]);
|
||||
? i18nService.collator.compare(a[prop] as unknown as string, b[prop] as unknown as string)
|
||||
: (a[prop] as unknown as string).localeCompare(b[prop] as unknown as string);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApiService } from "../abstractions/api.service";
|
||||
import { CipherService } from "../abstractions/cipher.service";
|
||||
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EventType } from "../enums/eventType";
|
||||
import { EventData } from "../models/data/eventData";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { OrganizationData } from "../models/data/organizationData";
|
||||
import { Organization } from "../models/domain/organization";
|
||||
|
||||
export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async get(id: string): Promise<Organization> {
|
||||
const organizations = await this.stateService.getOrganizations();
|
||||
// eslint-disable-next-line
|
||||
if (organizations == null || !organizations.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Organization(organizations[id]);
|
||||
}
|
||||
|
||||
async getByIdentifier(identifier: string): Promise<Organization> {
|
||||
const organizations = await this.getAll();
|
||||
if (organizations == null || organizations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return organizations.find((o) => o.identifier === identifier);
|
||||
}
|
||||
|
||||
async getAll(userId?: string): Promise<Organization[]> {
|
||||
const organizations = await this.stateService.getOrganizations({ userId: userId });
|
||||
const response: Organization[] = [];
|
||||
for (const id in organizations) {
|
||||
// eslint-disable-next-line
|
||||
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
|
||||
response.push(new Organization(organizations[id]));
|
||||
}
|
||||
}
|
||||
const sortedResponse = response.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return sortedResponse;
|
||||
}
|
||||
|
||||
async save(organizations: { [id: string]: OrganizationData }) {
|
||||
return await this.stateService.setOrganizations(organizations);
|
||||
}
|
||||
|
||||
async canManageSponsorships(): Promise<boolean> {
|
||||
const orgs = await this.getAll();
|
||||
return orgs.some(
|
||||
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
|
||||
);
|
||||
}
|
||||
|
||||
async hasOrganizations(userId?: string): Promise<boolean> {
|
||||
const organizations = await this.getAll(userId);
|
||||
return organizations.length > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||
import { SyncService } from "../../abstractions/sync/sync.service.abstraction";
|
||||
import { OrganizationApiKeyType } from "../../enums/organizationApiKeyType";
|
||||
import { ImportDirectoryRequest } from "../../models/request/importDirectoryRequest";
|
||||
import { OrganizationSsoRequest } from "../../models/request/organization/organizationSsoRequest";
|
||||
@@ -28,7 +29,7 @@ import { PaymentResponse } from "../../models/response/paymentResponse";
|
||||
import { TaxInfoResponse } from "../../models/response/taxInfoResponse";
|
||||
|
||||
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
constructor(private apiService: ApiService, private syncService: SyncService) {}
|
||||
|
||||
async get(id: string): Promise<OrganizationResponse> {
|
||||
const r = await this.apiService.send("GET", "/organizations/" + id, null, true, true);
|
||||
@@ -80,6 +81,8 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
|
||||
async create(request: OrganizationCreateRequest): Promise<OrganizationResponse> {
|
||||
const r = await this.apiService.send("POST", "/organizations", request, true, true);
|
||||
// Forcing a sync will notify organization service that they need to repull
|
||||
await this.syncService.fullSync(true);
|
||||
return new OrganizationResponse(r);
|
||||
}
|
||||
|
||||
@@ -90,7 +93,9 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
|
||||
async save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse> {
|
||||
const r = await this.apiService.send("PUT", "/organizations/" + id, request, true, true);
|
||||
return new OrganizationResponse(r);
|
||||
const data = new OrganizationResponse(r);
|
||||
await this.syncService.fullSync(true);
|
||||
return data;
|
||||
}
|
||||
|
||||
async updatePayment(id: string, request: PaymentRequest): Promise<void> {
|
||||
@@ -144,7 +149,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
}
|
||||
|
||||
async verifyBank(id: string, request: VerifyBankRequest): Promise<void> {
|
||||
return this.apiService.send(
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/verify-bank",
|
||||
request,
|
||||
@@ -162,15 +167,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
}
|
||||
|
||||
async leave(id: string): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false);
|
||||
await this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false);
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
async delete(id: string, request: SecretVerificationRequest): Promise<void> {
|
||||
return this.apiService.send("DELETE", "/organizations/" + id, request, true, false);
|
||||
await this.apiService.send("DELETE", "/organizations/" + id, request, true, false);
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
async updateLicense(id: string, data: FormData): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
|
||||
await this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
|
||||
}
|
||||
|
||||
async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void> {
|
||||
@@ -223,6 +230,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
}
|
||||
|
||||
async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise<void> {
|
||||
// Can't broadcast anything because the response doesn't have content
|
||||
return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false);
|
||||
}
|
||||
|
||||
@@ -242,6 +250,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
true,
|
||||
true
|
||||
);
|
||||
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
|
||||
return new OrganizationKeysResponse(r);
|
||||
}
|
||||
|
||||
@@ -258,6 +267,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
true,
|
||||
true
|
||||
);
|
||||
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
|
||||
return new OrganizationSsoResponse(r);
|
||||
}
|
||||
}
|
||||
|
||||
119
libs/common/src/services/organization/organization.service.ts
Normal file
119
libs/common/src/services/organization/organization.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BehaviorSubject, concatMap, filter } from "rxjs";
|
||||
|
||||
import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { OrganizationData } from "../../models/data/organizationData";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { isSuccessfullyCompleted } from "../../types/syncEventArgs";
|
||||
|
||||
export class OrganizationService implements OrganizationServiceAbstraction {
|
||||
private _organizations = new BehaviorSubject<Organization[]>([]);
|
||||
|
||||
organizations$ = this._organizations.asObservable();
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private syncNotifierService: SyncNotifierService
|
||||
) {
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
if (!unlocked) {
|
||||
this._organizations.next([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.stateService.getOrganizations();
|
||||
this.updateObservables(data);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.syncNotifierService.sync$
|
||||
.pipe(
|
||||
filter(isSuccessfullyCompleted),
|
||||
concatMap(async ({ data }) => {
|
||||
const { profile } = data;
|
||||
const organizations: { [id: string]: OrganizationData } = {};
|
||||
profile.organizations.forEach((o) => {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
});
|
||||
|
||||
profile.providerOrganizations.forEach((o) => {
|
||||
if (organizations[o.id] == null) {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
organizations[o.id].isProviderUser = true;
|
||||
}
|
||||
});
|
||||
|
||||
await this.updateStateAndObservables(organizations);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async getAll(userId?: string): Promise<Organization[]> {
|
||||
const organizationsMap = await this.stateService.getOrganizations({ userId: userId });
|
||||
return Object.values(organizationsMap || {}).map((o) => new Organization(o));
|
||||
}
|
||||
|
||||
async canManageSponsorships(): Promise<boolean> {
|
||||
const organizations = this._organizations.getValue();
|
||||
return organizations.some(
|
||||
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
|
||||
);
|
||||
}
|
||||
|
||||
hasOrganizations(): boolean {
|
||||
const organizations = this._organizations.getValue();
|
||||
return organizations.length > 0;
|
||||
}
|
||||
|
||||
async upsert(organization: OrganizationData): Promise<void> {
|
||||
let organizations = await this.stateService.getOrganizations();
|
||||
if (organizations == null) {
|
||||
organizations = {};
|
||||
}
|
||||
|
||||
organizations[organization.id] = organization;
|
||||
|
||||
await this.updateStateAndObservables(organizations);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const organizations = await this.stateService.getOrganizations();
|
||||
if (organizations == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (organizations[id] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete organizations[id];
|
||||
await this.updateStateAndObservables(organizations);
|
||||
}
|
||||
|
||||
get(id: string): Organization {
|
||||
const organizations = this._organizations.getValue();
|
||||
|
||||
return organizations.find((organization) => organization.id === id);
|
||||
}
|
||||
|
||||
getByIdentifier(identifier: string): Organization {
|
||||
const organizations = this._organizations.getValue();
|
||||
|
||||
return organizations.find((organization) => organization.identifier === identifier);
|
||||
}
|
||||
|
||||
private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) {
|
||||
await this.stateService.setOrganizations(organizationsMap);
|
||||
this.updateObservables(organizationsMap);
|
||||
}
|
||||
|
||||
private updateObservables(organizationsMap: { [id: string]: OrganizationData }) {
|
||||
const organizations = Object.values(organizationsMap || {}).map((o) => new Organization(o));
|
||||
this._organizations.next(organizations);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationService } from "../../abstractions/organization.service";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
|
||||
|
||||
@@ -1927,12 +1927,18 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Do not call this directly, use OrganizationService
|
||||
*/
|
||||
async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.data?.organizations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Do not call this directly, use OrganizationService
|
||||
*/
|
||||
async setOrganizations(
|
||||
value: { [id: string]: OrganizationData },
|
||||
options?: StorageOptions
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { CipherService } from "../../abstractions/cipher.service";
|
||||
import { CollectionService } from "../../abstractions/collection.service";
|
||||
@@ -9,18 +7,17 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service.
|
||||
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessagingService } from "../../abstractions/messaging.service";
|
||||
import { OrganizationService } from "../../abstractions/organization.service";
|
||||
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "../../abstractions/provider.service";
|
||||
import { SendService } from "../../abstractions/send.service";
|
||||
import { SettingsService } from "../../abstractions/settings.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction";
|
||||
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { sequentialize } from "../../misc/sequentialize";
|
||||
import { CipherData } from "../../models/data/cipherData";
|
||||
import { CollectionData } from "../../models/data/collectionData";
|
||||
import { FolderData } from "../../models/data/folderData";
|
||||
import { OrganizationData } from "../../models/data/organizationData";
|
||||
import { PolicyData } from "../../models/data/policyData";
|
||||
import { ProviderData } from "../../models/data/providerData";
|
||||
import { SendData } from "../../models/data/sendData";
|
||||
@@ -36,15 +33,10 @@ import {
|
||||
import { PolicyResponse } from "../../models/response/policyResponse";
|
||||
import { ProfileResponse } from "../../models/response/profileResponse";
|
||||
import { SendResponse } from "../../models/response/sendResponse";
|
||||
import { SyncEventArgs } from "../../types/syncEventArgs";
|
||||
|
||||
export class SyncService implements SyncServiceAbstraction {
|
||||
syncInProgress = false;
|
||||
|
||||
private _sync = new Subject<SyncEventArgs>();
|
||||
|
||||
sync$ = this._sync.asObservable();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private settingsService: SettingsService,
|
||||
@@ -58,9 +50,9 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
private logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private providerService: ProviderService,
|
||||
private folderApiService: FolderApiServiceAbstraction,
|
||||
private syncNotifierService: SyncNotifierService,
|
||||
private logoutCallback: (expired: boolean) => Promise<void>
|
||||
) {}
|
||||
|
||||
@@ -84,8 +76,10 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
@sequentialize(() => "fullSync")
|
||||
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
this.syncNotifierService.next({ status: "Started" });
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
@@ -101,6 +95,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
|
||||
if (!needsSync) {
|
||||
await this.setLastSync(now);
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
@@ -117,11 +112,13 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.syncPolicies(response.policies);
|
||||
|
||||
await this.setLastSync(now);
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: true, data: response });
|
||||
return this.syncCompleted(true);
|
||||
} catch (e) {
|
||||
if (allowThrowOnError) {
|
||||
throw e;
|
||||
} else {
|
||||
this.syncNotifierService.next({ status: "Completed", successfully: false });
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
}
|
||||
@@ -272,13 +269,11 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
private syncStarted() {
|
||||
this.syncInProgress = true;
|
||||
this.messagingService.send("syncStarted");
|
||||
this._sync.next({ status: "Started" });
|
||||
}
|
||||
|
||||
private syncCompleted(successfully: boolean): boolean {
|
||||
this.syncInProgress = false;
|
||||
this.messagingService.send("syncCompleted", { successfully: successfully });
|
||||
this._sync.next({ status: successfully ? "SuccessfullyCompleted" : "UnsuccessfullyCompleted" });
|
||||
return successfully;
|
||||
}
|
||||
|
||||
@@ -320,24 +315,11 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
|
||||
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
|
||||
|
||||
const organizations: { [id: string]: OrganizationData } = {};
|
||||
response.organizations.forEach((o) => {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
});
|
||||
|
||||
const providers: { [id: string]: ProviderData } = {};
|
||||
response.providers.forEach((p) => {
|
||||
providers[p.id] = new ProviderData(p);
|
||||
});
|
||||
|
||||
response.providerOrganizations.forEach((o) => {
|
||||
if (organizations[o.id] == null) {
|
||||
organizations[o.id] = new OrganizationData(o);
|
||||
organizations[o.id].isProviderUser = true;
|
||||
}
|
||||
});
|
||||
|
||||
await this.organizationService.save(organizations);
|
||||
await this.providerService.save(providers);
|
||||
|
||||
if (await this.keyConnectorService.userNeedsMigration()) {
|
||||
|
||||
18
libs/common/src/services/sync/syncNotifier.service.ts
Normal file
18
libs/common/src/services/sync/syncNotifier.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { SyncEventArgs } from "../../types/syncEventArgs";
|
||||
|
||||
/**
|
||||
* This class should most likely have 0 dependencies because it will hopefully
|
||||
* be rolled into SyncService once upon a time.
|
||||
*/
|
||||
export class SyncNotifierService implements SyncNotifierServiceAbstraction {
|
||||
private _sync = new Subject<SyncEventArgs>();
|
||||
|
||||
sync$ = this._sync.asObservable();
|
||||
|
||||
next(event: SyncEventArgs): void {
|
||||
this._sync.next(event);
|
||||
}
|
||||
}
|
||||
9
libs/common/src/types/checkable.ts
Normal file
9
libs/common/src/types/checkable.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type CheckableBase = {
|
||||
checked?: boolean;
|
||||
};
|
||||
|
||||
export type Checkable<T> = T & CheckableBase;
|
||||
|
||||
export function isChecked(item: CheckableBase): boolean {
|
||||
return !!item.checked;
|
||||
}
|
||||
@@ -1,15 +1,38 @@
|
||||
import { filter } from "rxjs";
|
||||
import { SyncResponse } from "../models/response/syncResponse";
|
||||
|
||||
export type SyncStatus = "Started" | "SuccessfullyCompleted" | "UnsuccessfullyCompleted";
|
||||
type SyncStatus = "Started" | "Completed";
|
||||
|
||||
export type SyncEventArgs = {
|
||||
status: SyncStatus;
|
||||
type SyncEventArgsBase<T extends SyncStatus> = {
|
||||
status: T;
|
||||
};
|
||||
|
||||
type SyncCompletedEventArgsBase<T extends boolean> = SyncEventArgsBase<"Completed"> & {
|
||||
successfully: T;
|
||||
};
|
||||
|
||||
type SyncSuccessfullyCompletedEventArgs = SyncCompletedEventArgsBase<true> & {
|
||||
data: SyncResponse;
|
||||
};
|
||||
|
||||
export type SyncEventArgs =
|
||||
| SyncSuccessfullyCompletedEventArgs
|
||||
| SyncCompletedEventArgsBase<false>
|
||||
| SyncEventArgsBase<"Started">;
|
||||
|
||||
/**
|
||||
* Helper function to filter only on successfully completed syncs
|
||||
* @returns a function that can be used in a `.pipe()` from an observable
|
||||
* @returns a function that can be used in a `.pipe(filter(...))` from an observable
|
||||
* @example
|
||||
* ```
|
||||
* of<SyncEventArgs>({ status: "Completed", successfully: true, data: new SyncResponse() })
|
||||
* .pipe(filter(isSuccessfullyCompleted))
|
||||
* .subscribe(event => {
|
||||
* console.log(event.data);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function onlySuccessfullyCompleted() {
|
||||
return filter<SyncEventArgs>((syncEvent) => syncEvent.status === "SuccessfullyCompleted");
|
||||
export function isSuccessfullyCompleted(
|
||||
syncEvent: SyncEventArgs
|
||||
): syncEvent is SyncSuccessfullyCompletedEventArgs {
|
||||
return syncEvent.status === "Completed" && syncEvent.successfully;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user