1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +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:
Justin Baur
2022-09-27 16:25:19 -04:00
committed by GitHub
parent 2c68518f87
commit c6dccc354c
100 changed files with 1225 additions and 813 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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

View File

@@ -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()) {

View 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);
}
}