mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-24146] Remove stateProvider.activeUserId from ProviderService (#16258)
* Refactor provider service calls to include userId parameter - Updated multiple components and services to pass userId when fetching provider data. - Adjusted the ProviderService interface to require userId for get, get$, and getAll methods. - Ensured consistent handling of userId across various components, enhancing data retrieval based on active user context. * Remove deprecated type safety comments and use the getById utility for fetching providers. * Update ProviderService methods to return undefined for non-existent providers - Modified the return types of get$ and get methods in ProviderService to allow for undefined values, enhancing type safety. - Adjusted the providers$ method to return only defined Provider arrays, ensuring consistent handling of provider data. * Enhance provider permissions guard tests to include userId parameter - Updated test cases in provider-permissions.guard.spec.ts to pass userId when calling ProviderService methods. - Mocked AccountService to provide active account details for improved test coverage. - Ensured consistent handling of userId across all relevant test scenarios. * remove promise based api's from provider service, continue refactor * cleanup observable logic * cleanup --------- Co-authored-by: Brandon <btreston@bitwarden.com>
This commit is contained in:
@@ -121,8 +121,13 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
);
|
||||
|
||||
const provider$ = this.organization$.pipe(
|
||||
switchMap((organization) => this.providerService.get$(organization.providerId)),
|
||||
const provider$ = combineLatest([
|
||||
this.organization$,
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
]).pipe(
|
||||
switchMap(([organization, userId]) =>
|
||||
this.providerService.get$(organization.providerId, userId),
|
||||
),
|
||||
);
|
||||
|
||||
this.organizationIsUnmanaged$ = combineLatest([this.organization$, provider$]).pipe(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, firstValueFrom, lastValueFrom, takeUntil } from "rxjs";
|
||||
import { concatMap, filter, firstValueFrom, lastValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
@@ -136,22 +136,24 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
|
||||
|
||||
if (this.organization.providerId != null) {
|
||||
try {
|
||||
const provider = await this.providerService.get(this.organization.providerId);
|
||||
if (
|
||||
provider != null &&
|
||||
(await this.providerService.get(this.organization.providerId)).canManageUsers
|
||||
) {
|
||||
const providerUsersResponse = await this.apiService.getProviderUsers(
|
||||
this.organization.providerId,
|
||||
);
|
||||
providerUsersResponse.data.forEach((u) => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.orgUsersUserIdMap.set(u.userId, {
|
||||
name: `${name} (${this.organization.providerName})`,
|
||||
email: u.email,
|
||||
});
|
||||
});
|
||||
}
|
||||
await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.providerService.get$(this.organization.providerId, userId)),
|
||||
map((provider) => provider != null && provider.canManageUsers),
|
||||
filter((result) => result),
|
||||
switchMap(() => this.apiService.getProviderUsers(this.organization.id)),
|
||||
map((providerUsersResponse) =>
|
||||
providerUsersResponse.data.forEach((u) => {
|
||||
const name = this.userNamePipe.transform(u);
|
||||
this.orgUsersUserIdMap.set(u.userId, {
|
||||
name: `${name} (${this.organization.providerName})`,
|
||||
email: u.email,
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.warning(e);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRou
|
||||
return true;
|
||||
}
|
||||
|
||||
const provider = await providerService.get(organization.providerId);
|
||||
const provider = await firstValueFrom(providerService.get$(organization.providerId, userId));
|
||||
|
||||
if (!provider) {
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -47,8 +47,8 @@ class MockOrganizationService implements Partial<OrganizationService> {
|
||||
class MockProviderService implements Partial<ProviderService> {
|
||||
private static _providers = new BehaviorSubject<Provider[]>([]);
|
||||
|
||||
async getAll() {
|
||||
return await firstValueFrom(MockProviderService._providers);
|
||||
providers$() {
|
||||
return MockProviderService._providers.asObservable();
|
||||
}
|
||||
|
||||
@Input()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Directive, importProvidersFrom, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -47,8 +47,8 @@ class MockOrganizationService implements Partial<OrganizationService> {
|
||||
class MockProviderService implements Partial<ProviderService> {
|
||||
private static _providers = new BehaviorSubject<Provider[]>([]);
|
||||
|
||||
async getAll() {
|
||||
return await firstValueFrom(MockProviderService._providers);
|
||||
providers$() {
|
||||
return MockProviderService._providers.asObservable();
|
||||
}
|
||||
|
||||
@Input()
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("ProductSwitcherService", () => {
|
||||
router.url = "/";
|
||||
router.events = of({});
|
||||
organizationService.organizations$.mockReturnValue(of([{}] as Organization[]));
|
||||
providerService.getAll.mockResolvedValue([] as Provider[]);
|
||||
providerService.providers$.mockReturnValue(of([]) as Observable<Provider[]>);
|
||||
platformUtilsService.isSelfHost.mockReturnValue(false);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -212,7 +212,7 @@ describe("ProductSwitcherService", () => {
|
||||
});
|
||||
|
||||
it("is included when there are providers", async () => {
|
||||
providerService.getAll.mockResolvedValue([{ id: "67899" }] as Provider[]);
|
||||
providerService.providers$.mockReturnValue(of([{ id: "67899" }]) as Observable<Provider[]>);
|
||||
|
||||
initiateService();
|
||||
|
||||
@@ -263,7 +263,7 @@ describe("ProductSwitcherService", () => {
|
||||
});
|
||||
|
||||
it("marks Provider Portal as active", async () => {
|
||||
providerService.getAll.mockResolvedValue([{ id: "67899" }] as Provider[]);
|
||||
providerService.providers$.mockReturnValue(of([{ id: "67899" }]) as Observable<Provider[]>);
|
||||
router.url = "/providers/";
|
||||
|
||||
initiateService();
|
||||
|
||||
@@ -2,17 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationEnd, NavigationStart, ParamMap, Router } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { combineLatest, filter, map, Observable, ReplaySubject, startWith, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
@@ -22,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { PolicyType, ProviderType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -117,148 +108,159 @@ export class ProductSwitcherService {
|
||||
switchMap((id) => this.organizationService.organizations$(id)),
|
||||
);
|
||||
|
||||
providers$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((id) => this.providerService.providers$(id)),
|
||||
);
|
||||
|
||||
userHasSingleOrgPolicy$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
);
|
||||
|
||||
products$: Observable<{
|
||||
bento: ProductSwitcherItem[];
|
||||
other: ProductSwitcherItem[];
|
||||
}> = combineLatest([this.organizations$, this.route.paramMap, this.triggerProductUpdate$]).pipe(
|
||||
map(([orgs, ...rest]): [Organization[], ParamMap, void] => {
|
||||
return [
|
||||
}> = combineLatest([
|
||||
this.organizations$,
|
||||
this.providers$,
|
||||
this.userHasSingleOrgPolicy$,
|
||||
this.route.paramMap,
|
||||
this.triggerProductUpdate$,
|
||||
]).pipe(
|
||||
map(
|
||||
([orgs, providers, userHasSingleOrgPolicy, paramMap]: [
|
||||
Organization[],
|
||||
Provider[],
|
||||
boolean,
|
||||
ParamMap,
|
||||
void,
|
||||
]) => {
|
||||
// Sort orgs by name to match the order within the sidebar
|
||||
orgs.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
...rest,
|
||||
];
|
||||
}),
|
||||
concatMap(async ([orgs, paramMap]) => {
|
||||
let routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
||||
orgs.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
let organizationIdViaPath: string | null = null;
|
||||
let routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId"));
|
||||
|
||||
if (["/sm/", "/organizations/"].some((path) => this.router.url.includes(path))) {
|
||||
// Grab the organization ID from the URL
|
||||
organizationIdViaPath = this.router.url.split("/")[2] ?? null;
|
||||
}
|
||||
let organizationIdViaPath: string | null = null;
|
||||
|
||||
// When the user is already viewing an organization within an application use it as the active route org
|
||||
if (organizationIdViaPath && !routeOrg) {
|
||||
routeOrg = orgs.find((o) => o.id === organizationIdViaPath);
|
||||
}
|
||||
|
||||
// If the active route org doesn't have access to SM, find the first org that does.
|
||||
const smOrg =
|
||||
routeOrg?.canAccessSecretsManager && routeOrg?.enabled == true
|
||||
? routeOrg
|
||||
: orgs.find((o) => o.canAccessSecretsManager && o.enabled == true);
|
||||
|
||||
// If the active route org doesn't have access to AC, find the first org that does.
|
||||
const acOrg =
|
||||
routeOrg != null && canAccessOrgAdmin(routeOrg)
|
||||
? routeOrg
|
||||
: orgs.find((o) => canAccessOrgAdmin(o));
|
||||
|
||||
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
|
||||
const providers = await this.providerService.getAll();
|
||||
|
||||
const providerPortalName =
|
||||
providers[0]?.providerType === ProviderType.BusinessUnit
|
||||
? "Business Unit Portal"
|
||||
: "Provider Portal";
|
||||
|
||||
const orgsMarketingRoute = this.platformUtilsService.isSelfHost()
|
||||
? {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
}
|
||||
: {
|
||||
route: "/create-organization",
|
||||
external: false,
|
||||
};
|
||||
|
||||
const products = {
|
||||
pm: {
|
||||
name: "Password Manager",
|
||||
icon: "bwi-lock",
|
||||
appRoute: "/vault",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/personal/",
|
||||
external: true,
|
||||
},
|
||||
isActive:
|
||||
!this.router.url.includes("/sm/") &&
|
||||
!this.router.url.includes("/organizations/") &&
|
||||
!this.router.url.includes("/providers/"),
|
||||
},
|
||||
sm: {
|
||||
name: "Secrets Manager",
|
||||
icon: "bwi-cli",
|
||||
appRoute: ["/sm", smOrg?.id],
|
||||
marketingRoute: {
|
||||
route: "/sm-landing",
|
||||
external: false,
|
||||
},
|
||||
isActive: this.router.url.includes("/sm/"),
|
||||
otherProductOverrides: {
|
||||
supportingText: this.i18nService.t("secureYourInfrastructure"),
|
||||
},
|
||||
},
|
||||
ac: {
|
||||
name: "Admin Console",
|
||||
icon: "bwi-business",
|
||||
appRoute: ["/organizations", acOrg?.id],
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
},
|
||||
isActive: this.router.url.includes("/organizations/"),
|
||||
},
|
||||
provider: {
|
||||
name: providerPortalName,
|
||||
icon: "bwi-provider",
|
||||
appRoute: ["/providers", providers[0]?.id],
|
||||
isActive: this.router.url.includes("/providers/"),
|
||||
},
|
||||
orgs: {
|
||||
name: "Organizations",
|
||||
icon: "bwi-business",
|
||||
marketingRoute: orgsMarketingRoute,
|
||||
otherProductOverrides: {
|
||||
name: "Share your passwords",
|
||||
supportingText: this.i18nService.t("protectYourFamilyOrBusiness"),
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, ProductSwitcherItem>;
|
||||
|
||||
const bento: ProductSwitcherItem[] = [products.pm];
|
||||
const other: ProductSwitcherItem[] = [];
|
||||
|
||||
if (smOrg) {
|
||||
bento.push(products.sm);
|
||||
} else {
|
||||
other.push(products.sm);
|
||||
}
|
||||
|
||||
if (acOrg) {
|
||||
bento.push(products.ac);
|
||||
} else {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
const userHasSingleOrgPolicy = await firstValueFrom(
|
||||
this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, activeUserId),
|
||||
);
|
||||
if (!userHasSingleOrgPolicy) {
|
||||
other.push(products.orgs);
|
||||
if (["/sm/", "/organizations/"].some((path) => this.router.url.includes(path))) {
|
||||
// Grab the organization ID from the URL
|
||||
organizationIdViaPath = this.router.url.split("/")[2] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (providers.length > 0) {
|
||||
bento.push(products.provider);
|
||||
}
|
||||
// When the user is already viewing an organization within an application use it as the active route org
|
||||
if (organizationIdViaPath && !routeOrg) {
|
||||
routeOrg = orgs.find((o) => o.id === organizationIdViaPath);
|
||||
}
|
||||
|
||||
return {
|
||||
bento,
|
||||
other,
|
||||
};
|
||||
}),
|
||||
// If the active route org doesn't have access to SM, find the first org that does.
|
||||
const smOrg =
|
||||
routeOrg?.canAccessSecretsManager && routeOrg?.enabled == true
|
||||
? routeOrg
|
||||
: orgs.find((o) => o.canAccessSecretsManager && o.enabled == true);
|
||||
|
||||
// If the active route org doesn't have access to AC, find the first org that does.
|
||||
const acOrg =
|
||||
routeOrg != null && canAccessOrgAdmin(routeOrg)
|
||||
? routeOrg
|
||||
: orgs.find((o) => canAccessOrgAdmin(o));
|
||||
|
||||
const providerPortalName =
|
||||
providers[0]?.providerType === ProviderType.BusinessUnit
|
||||
? "Business Unit Portal"
|
||||
: "Provider Portal";
|
||||
|
||||
const orgsMarketingRoute = this.platformUtilsService.isSelfHost()
|
||||
? {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
}
|
||||
: {
|
||||
route: "/create-organization",
|
||||
external: false,
|
||||
};
|
||||
|
||||
const products = {
|
||||
pm: {
|
||||
name: "Password Manager",
|
||||
icon: "bwi-lock",
|
||||
appRoute: "/vault",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/personal/",
|
||||
external: true,
|
||||
},
|
||||
isActive:
|
||||
!this.router.url.includes("/sm/") &&
|
||||
!this.router.url.includes("/organizations/") &&
|
||||
!this.router.url.includes("/providers/"),
|
||||
},
|
||||
sm: {
|
||||
name: "Secrets Manager",
|
||||
icon: "bwi-cli",
|
||||
appRoute: ["/sm", smOrg?.id],
|
||||
marketingRoute: {
|
||||
route: "/sm-landing",
|
||||
external: false,
|
||||
},
|
||||
isActive: this.router.url.includes("/sm/"),
|
||||
otherProductOverrides: {
|
||||
supportingText: this.i18nService.t("secureYourInfrastructure"),
|
||||
},
|
||||
},
|
||||
ac: {
|
||||
name: "Admin Console",
|
||||
icon: "bwi-business",
|
||||
appRoute: ["/organizations", acOrg?.id],
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
external: true,
|
||||
},
|
||||
isActive: this.router.url.includes("/organizations/"),
|
||||
},
|
||||
provider: {
|
||||
name: providerPortalName,
|
||||
icon: "bwi-provider",
|
||||
appRoute: ["/providers", providers[0]?.id],
|
||||
isActive: this.router.url.includes("/providers/"),
|
||||
},
|
||||
orgs: {
|
||||
name: "Organizations",
|
||||
icon: "bwi-business",
|
||||
marketingRoute: orgsMarketingRoute,
|
||||
otherProductOverrides: {
|
||||
name: "Share your passwords",
|
||||
supportingText: this.i18nService.t("protectYourFamilyOrBusiness"),
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, ProductSwitcherItem>;
|
||||
|
||||
const bento: ProductSwitcherItem[] = [products.pm];
|
||||
const other: ProductSwitcherItem[] = [];
|
||||
|
||||
if (smOrg) {
|
||||
bento.push(products.sm);
|
||||
} else {
|
||||
other.push(products.sm);
|
||||
}
|
||||
|
||||
if (acOrg) {
|
||||
bento.push(products.ac);
|
||||
} else {
|
||||
if (!userHasSingleOrgPolicy) {
|
||||
other.push(products.orgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (providers.length > 0) {
|
||||
bento.push(products.provider);
|
||||
}
|
||||
|
||||
return {
|
||||
bento,
|
||||
other,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/** Poll the `syncService` until a sync is completed */
|
||||
|
||||
Reference in New Issue
Block a user