diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html
new file mode 100644
index 00000000000..6c04ea87960
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.html
@@ -0,0 +1,11 @@
+@let integrationsList = integrations();
+
+
+
+ {{ "deviceManagement" | i18n }}
+
+ {{ "deviceManagementDesc" | i18n }}
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts
new file mode 100644
index 00000000000..18e6dc7e362
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/device-management/device-management.component.ts
@@ -0,0 +1,25 @@
+import { Component } from "@angular/core";
+
+import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
+import { FilterIntegrationsPipe } from "../integrations.pipe";
+import { OrganizationIntegrationsState } from "../organization-integrations.state";
+
+// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
+// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
+@Component({
+ selector: "device-management",
+ templateUrl: "device-management.component.html",
+ imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
+})
+export class DeviceManagementComponent {
+ integrations = this.state.integrations;
+
+ constructor(private state: OrganizationIntegrationsState) {}
+
+ get IntegrationType(): typeof IntegrationType {
+ return IntegrationType;
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html
new file mode 100644
index 00000000000..9a767e52c8b
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.html
@@ -0,0 +1,11 @@
+@let integrationsList = integrations();
+
+
+
+ {{ "eventManagement" | i18n }}
+
+ {{ "eventManagementDesc" | i18n }}
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts
new file mode 100644
index 00000000000..70b17cabd35
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/event-management/event-management.component.ts
@@ -0,0 +1,24 @@
+import { Component } from "@angular/core";
+
+import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
+import { FilterIntegrationsPipe } from "../integrations.pipe";
+import { OrganizationIntegrationsState } from "../organization-integrations.state";
+
+// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
+// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
+@Component({
+ selector: "event-management",
+ templateUrl: "event-management.component.html",
+ imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
+})
+export class EventManagementComponent {
+ integrations = this.state.integrations;
+ constructor(private state: OrganizationIntegrationsState) {}
+
+ get IntegrationType(): typeof IntegrationType {
+ return IntegrationType;
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html
index 14f20a0b71c..fbff31f026e 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.html
@@ -1,82 +1,18 @@
-
+@let org = organization();
-@let organization = organization$ | async;
+
+ @if (org) {
+
+ {{ "singleSignOn" | i18n }}
+ @if (org.useScim || org.useDirectory) {
+ {{ "userProvisioning" | i18n }}
+ }
+ @if (org.useEvents) {
+ {{ "eventManagement" | i18n }}
+ }
+ {{ "deviceManagement" | i18n }}
+
+ }
+
-@if (organization) {
-
- @if (organization?.useSso) {
-
-
-
- }
-
- @if (organization?.useScim || organization?.useDirectory) {
-
- @if (organization?.useScim) {
-
-
- {{ "scimIntegration" | i18n }}
-
-
- {{ "scimIntegrationDescStart" | i18n }}
- {{ "scimIntegration" | i18n }}
- {{ "scimIntegrationDescEnd" | i18n }}
-
-
-
- }
- @if (organization?.useDirectory) {
-
-
- {{ "bwdc" | i18n }}
-
- {{ "bwdcDesc" | i18n }}
-
-
- }
-
- }
-
- @if (organization?.useEvents) {
-
-
-
- {{ "eventManagement" | i18n }}
-
- {{ "eventManagementDesc" | i18n }}
-
-
-
- }
-
-
-
-
- {{ "deviceManagement" | i18n }}
-
- {{ "deviceManagementDesc" | i18n }}
-
-
-
-
-}
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts
index 5485410f735..786aa70bfc5 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts
@@ -1,336 +1,22 @@
-import { Component, OnDestroy, OnInit } from "@angular/core";
-import { ActivatedRoute } from "@angular/router";
-import { firstValueFrom, Observable, Subject, switchMap, takeUntil, takeWhile } from "rxjs";
+import { Component } from "@angular/core";
-import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
-import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
-import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
-import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
-import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
-import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { getUserId } from "@bitwarden/common/auth/services/account.service";
-import { IntegrationType } from "@bitwarden/common/enums";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-import { getById } from "@bitwarden/common/platform/misc";
+import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
-import { IntegrationGridComponent } from "./integration-grid/integration-grid.component";
-import { FilterIntegrationsPipe } from "./integrations.pipe";
+import { OrganizationIntegrationsState } from "./organization-integrations.state";
-// attempted, but because bit-tab-group is not OnPush, caused more issues than it solved
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "ac-integrations",
templateUrl: "./integrations.component.html",
- imports: [SharedModule, IntegrationGridComponent, HeaderModule, FilterIntegrationsPipe],
+ imports: [SharedModule, HeaderModule],
})
-export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
- tabIndex: number = 0;
- organization$: Observable
= new Observable();
- isEventManagementForDataDogAndCrowdStrikeEnabled: boolean = false;
- isEventManagementForHuntressEnabled: boolean = false;
- private destroy$ = new Subject();
+export class AdminConsoleIntegrationsComponent {
+ organization = this.state.organization;
- // initialize the integrations list with default integrations
- integrationsList: Integration[] = [
- {
- name: "AD FS",
- linkURL: "https://bitwarden.com/help/saml-adfs/",
- image: "../../../../../../../images/integrations/azure-active-directory.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Auth0",
- linkURL: "https://bitwarden.com/help/saml-auth0/",
- image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "AWS",
- linkURL: "https://bitwarden.com/help/saml-aws/",
- image: "../../../../../../../images/integrations/aws-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Microsoft Entra ID",
- linkURL: "https://bitwarden.com/help/saml-azure/",
- image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Duo",
- linkURL: "https://bitwarden.com/help/saml-duo/",
- image: "../../../../../../../images/integrations/logo-duo-color.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Google",
- linkURL: "https://bitwarden.com/help/saml-google/",
- image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "JumpCloud",
- linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
- image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "KeyCloak",
- linkURL: "https://bitwarden.com/help/saml-keycloak/",
- image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Okta",
- linkURL: "https://bitwarden.com/help/saml-okta/",
- image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
- imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "OneLogin",
- linkURL: "https://bitwarden.com/help/saml-onelogin/",
- image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "PingFederate",
- linkURL: "https://bitwarden.com/help/saml-pingfederate/",
- image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
- type: IntegrationType.SSO,
- },
- {
- name: "Microsoft Entra ID",
- linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
- image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
- type: IntegrationType.SCIM,
- },
- {
- name: "Okta",
- linkURL: "https://bitwarden.com/help/okta-scim-integration/",
- image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
- imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
- type: IntegrationType.SCIM,
- },
- {
- name: "OneLogin",
- linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
- image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
- type: IntegrationType.SCIM,
- },
- {
- name: "JumpCloud",
- linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
- image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
- type: IntegrationType.SCIM,
- },
- {
- name: "Ping Identity",
- linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
- image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
- type: IntegrationType.SCIM,
- },
- {
- name: "Active Directory",
- linkURL: "https://bitwarden.com/help/ldap-directory/",
- image: "../../../../../../../images/integrations/azure-active-directory.svg",
- type: IntegrationType.BWDC,
- },
- {
- name: "Microsoft Entra ID",
- linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
- image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
- type: IntegrationType.BWDC,
- },
- {
- name: "Google Workspace",
- linkURL: "https://bitwarden.com/help/workspace-directory/",
- image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
- type: IntegrationType.BWDC,
- },
- {
- name: "Okta",
- linkURL: "https://bitwarden.com/help/okta-directory/",
- image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
- imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
- type: IntegrationType.BWDC,
- },
- {
- name: "OneLogin",
- linkURL: "https://bitwarden.com/help/onelogin-directory/",
- image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
- imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
- type: IntegrationType.BWDC,
- },
- {
- name: "Splunk",
- linkURL: "https://bitwarden.com/help/splunk-siem/",
- image: "../../../../../../../images/integrations/logo-splunk-black.svg",
- imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
- type: IntegrationType.EVENT,
- },
- {
- name: "Microsoft Sentinel",
- linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
- image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
- type: IntegrationType.EVENT,
- },
- {
- name: "Rapid7",
- linkURL: "https://bitwarden.com/help/rapid7-siem/",
- image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
- imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
- type: IntegrationType.EVENT,
- },
- {
- name: "Elastic",
- linkURL: "https://bitwarden.com/help/elastic-siem/",
- image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
- type: IntegrationType.EVENT,
- },
- {
- name: "Panther",
- linkURL: "https://bitwarden.com/help/panther-siem/",
- image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
- type: IntegrationType.EVENT,
- },
- {
- name: "Sumo Logic",
- linkURL: "https://bitwarden.com/help/sumo-logic-siem/",
- image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg",
- imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg",
- type: IntegrationType.EVENT,
- newBadgeExpiration: "2025-12-31",
- },
- {
- name: "Microsoft Intune",
- linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
- image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
- type: IntegrationType.DEVICE,
- },
- ];
-
- async ngOnInit() {
- const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
- if (!userId) {
- throw new Error("User ID not found");
- }
-
- this.organization$ = this.route.params.pipe(
- switchMap((params) =>
- this.organizationService.organizations$(userId).pipe(
- getById(params.organizationId),
- // Filter out undefined values
- takeWhile((org: Organization | undefined) => !!org),
- ),
- ),
- );
-
- // Sets the organization ID which also loads the integrations$
- this.organization$
- .pipe(
- switchMap((org) => this.organizationIntegrationService.setOrganizationId(org.id)),
- takeUntil(this.destroy$),
- )
- .subscribe();
- }
-
- constructor(
- private route: ActivatedRoute,
- private organizationService: OrganizationService,
- private accountService: AccountService,
- private configService: ConfigService,
- private organizationIntegrationService: OrganizationIntegrationService,
- ) {
- this.configService
- .getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike)
- .pipe(takeUntil(this.destroy$))
- .subscribe((isEnabled) => {
- this.isEventManagementForDataDogAndCrowdStrikeEnabled = isEnabled;
- });
-
- this.configService
- .getFeatureFlag$(FeatureFlag.EventManagementForHuntress)
- .pipe(takeUntil(this.destroy$))
- .subscribe((isEnabled) => {
- this.isEventManagementForHuntressEnabled = isEnabled;
- });
-
- // Add the new event based items to the list
- if (this.isEventManagementForDataDogAndCrowdStrikeEnabled) {
- const crowdstrikeIntegration: Integration = {
- name: OrganizationIntegrationServiceName.CrowdStrike,
- linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
- image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
- type: IntegrationType.EVENT,
- description: "crowdstrikeEventIntegrationDesc",
- canSetupConnection: true,
- integrationType: OrganizationIntegrationType.Hec,
- };
-
- this.integrationsList.push(crowdstrikeIntegration);
-
- const datadogIntegration: Integration = {
- name: OrganizationIntegrationServiceName.Datadog,
- linkURL: "https://bitwarden.com/help/datadog-siem/",
- image: "../../../../../../../images/integrations/logo-datadog-color.svg",
- type: IntegrationType.EVENT,
- description: "datadogEventIntegrationDesc",
- canSetupConnection: true,
- integrationType: OrganizationIntegrationType.Datadog,
- };
-
- this.integrationsList.push(datadogIntegration);
- }
-
- // Add Huntress SIEM integration (separate feature flag)
- if (this.isEventManagementForHuntressEnabled) {
- const huntressIntegration: Integration = {
- name: OrganizationIntegrationServiceName.Huntress,
- linkURL: "https://bitwarden.com/help/huntress-siem/",
- image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
- type: IntegrationType.EVENT,
- description: "huntressEventIntegrationDesc",
- canSetupConnection: true,
- integrationType: OrganizationIntegrationType.Hec,
- };
-
- this.integrationsList.push(huntressIntegration);
- }
-
- // For all existing event based configurations loop through and assign the
- // organizationIntegration for the correct services.
- this.organizationIntegrationService.integrations$
- .pipe(takeUntil(this.destroy$))
- .subscribe((integrations) => {
- // reset all event based integrations to null first - in case one was deleted
- this.integrationsList.forEach((i) => {
- i.organizationIntegration = null;
- });
-
- integrations.forEach((integration) => {
- const item = this.integrationsList.find((i) => i.name === integration.serviceName);
- if (item) {
- item.organizationIntegration = integration;
- }
- });
- });
- }
-
- ngOnDestroy(): void {
- this.destroy$.next();
- this.destroy$.complete();
- }
+ constructor(private state: OrganizationIntegrationsState) {}
// use in the view
get IntegrationType(): typeof IntegrationType {
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts
index 7a420ade4b5..10ee251a921 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.pipe.ts
@@ -7,7 +7,10 @@ import { IntegrationType } from "@bitwarden/common/enums";
name: "filterIntegrations",
})
export class FilterIntegrationsPipe implements PipeTransform {
- transform(integrations: Integration[], type: IntegrationType): Integration[] {
+ transform(integrations: Integration[] | null | undefined, type: IntegrationType): Integration[] {
+ if (!integrations) {
+ return [];
+ }
return integrations.filter((integration) => integration.type === type);
}
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts
index 1667689b186..626fc5dee88 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations-routing.module.ts
@@ -3,16 +3,31 @@ import { RouterModule, Routes } from "@angular/router";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
+import { DeviceManagementComponent } from "./device-management/device-management.component";
+import { EventManagementComponent } from "./event-management/event-management.component";
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
+import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
+import { OrganizationIntegrationsState } from "./organization-integrations.state";
+import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
+import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
const routes: Routes = [
{
path: "",
canActivate: [organizationPermissionsGuard((org) => org.canAccessIntegrations)],
- component: AdminConsoleIntegrationsComponent,
data: {
titleId: "integrations",
},
+ component: AdminConsoleIntegrationsComponent,
+ providers: [OrganizationIntegrationsState, OrganizationIntegrationsResolver],
+ resolve: { integrations: OrganizationIntegrationsResolver },
+ children: [
+ { path: "", redirectTo: "single-sign-on", pathMatch: "full" },
+ { path: "single-sign-on", component: SingleSignOnComponent },
+ { path: "user-provisioning", component: UserProvisioningComponent },
+ { path: "event-management", component: EventManagementComponent },
+ { path: "device-management", component: DeviceManagementComponent },
+ ],
},
];
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts
index 789ae548521..33f389a92a9 100644
--- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.module.ts
@@ -1,17 +1,30 @@
import { NgModule } from "@angular/core";
+import { DeviceManagementComponent } from "@bitwarden/angular/auth/device-management/device-management.component";
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { safeProvider } from "@bitwarden/ui-common";
+import { EventManagementComponent } from "./event-management/event-management.component";
import { AdminConsoleIntegrationsComponent } from "./integrations.component";
import { OrganizationIntegrationsRoutingModule } from "./organization-integrations-routing.module";
+import { OrganizationIntegrationsResolver } from "./organization-integrations.resolver";
+import { SingleSignOnComponent } from "./single-sign-on/single-sign-on.component";
+import { UserProvisioningComponent } from "./user-provisioning/user-provisioning.component";
@NgModule({
- imports: [AdminConsoleIntegrationsComponent, OrganizationIntegrationsRoutingModule],
+ imports: [
+ AdminConsoleIntegrationsComponent,
+ OrganizationIntegrationsRoutingModule,
+ SingleSignOnComponent,
+ UserProvisioningComponent,
+ DeviceManagementComponent,
+ EventManagementComponent,
+ ],
providers: [
+ OrganizationIntegrationsResolver,
safeProvider({
provide: OrganizationIntegrationService,
useClass: OrganizationIntegrationService,
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts
new file mode 100644
index 00000000000..39bd0cc1dcc
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.resolver.ts
@@ -0,0 +1,285 @@
+import { Injectable } from "@angular/core";
+import { ActivatedRouteSnapshot, Resolve } from "@angular/router";
+import { firstValueFrom } from "rxjs";
+import { take, takeWhile } from "rxjs/operators";
+
+import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
+import { OrganizationIntegrationServiceName } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-service-type";
+import { OrganizationIntegrationType } from "@bitwarden/bit-common/dirt/organization-integrations/models/organization-integration-type";
+import { OrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-service";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { IntegrationType } from "@bitwarden/common/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { getById } from "@bitwarden/common/platform/misc";
+
+import { OrganizationIntegrationsState } from "./organization-integrations.state";
+
+@Injectable()
+export class OrganizationIntegrationsResolver implements Resolve {
+ constructor(
+ private organizationService: OrganizationService,
+ private accountService: AccountService,
+ private configService: ConfigService,
+ private organizationIntegrationService: OrganizationIntegrationService,
+ private state: OrganizationIntegrationsState,
+ ) {}
+
+ async resolve(route: ActivatedRouteSnapshot): Promise {
+ const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
+
+ if (!userId) {
+ throw new Error("User ID not found");
+ }
+
+ const orgId = route.paramMap.get("organizationId")!;
+ const org = await firstValueFrom(
+ this.organizationService.organizations$(userId).pipe(getById(orgId), takeWhile(Boolean)),
+ );
+
+ this.state.setOrganization(org);
+
+ await firstValueFrom(this.organizationIntegrationService.setOrganizationId(org.id));
+
+ const integrations: Integration[] = [
+ {
+ name: "AD FS",
+ linkURL: "https://bitwarden.com/help/saml-adfs/",
+ image: "../../../../../../../images/integrations/azure-active-directory.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Auth0",
+ linkURL: "https://bitwarden.com/help/saml-auth0/",
+ image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "AWS",
+ linkURL: "https://bitwarden.com/help/saml-aws/",
+ image: "../../../../../../../images/integrations/aws-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Microsoft Entra ID",
+ linkURL: "https://bitwarden.com/help/saml-azure/",
+ image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Duo",
+ linkURL: "https://bitwarden.com/help/saml-duo/",
+ image: "../../../../../../../images/integrations/logo-duo-color.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Google",
+ linkURL: "https://bitwarden.com/help/saml-google/",
+ image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "JumpCloud",
+ linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
+ image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "KeyCloak",
+ linkURL: "https://bitwarden.com/help/saml-keycloak/",
+ image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Okta",
+ linkURL: "https://bitwarden.com/help/saml-okta/",
+ image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
+ imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "OneLogin",
+ linkURL: "https://bitwarden.com/help/saml-onelogin/",
+ image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "PingFederate",
+ linkURL: "https://bitwarden.com/help/saml-pingfederate/",
+ image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
+ type: IntegrationType.SSO,
+ },
+ {
+ name: "Microsoft Entra ID",
+ linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
+ image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
+ type: IntegrationType.SCIM,
+ },
+ {
+ name: "Okta",
+ linkURL: "https://bitwarden.com/help/okta-scim-integration/",
+ image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
+ imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
+ type: IntegrationType.SCIM,
+ },
+ {
+ name: "OneLogin",
+ linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
+ image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
+ type: IntegrationType.SCIM,
+ },
+ {
+ name: "JumpCloud",
+ linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
+ image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
+ type: IntegrationType.SCIM,
+ },
+ {
+ name: "Ping Identity",
+ linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
+ image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
+ type: IntegrationType.SCIM,
+ },
+ {
+ name: "Active Directory",
+ linkURL: "https://bitwarden.com/help/ldap-directory/",
+ image: "../../../../../../../images/integrations/azure-active-directory.svg",
+ type: IntegrationType.BWDC,
+ },
+ {
+ name: "Microsoft Entra ID",
+ linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
+ image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
+ type: IntegrationType.BWDC,
+ },
+ {
+ name: "Google Workspace",
+ linkURL: "https://bitwarden.com/help/workspace-directory/",
+ image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
+ type: IntegrationType.BWDC,
+ },
+ {
+ name: "Okta",
+ linkURL: "https://bitwarden.com/help/okta-directory/",
+ image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
+ imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
+ type: IntegrationType.BWDC,
+ },
+ {
+ name: "OneLogin",
+ linkURL: "https://bitwarden.com/help/onelogin-directory/",
+ image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
+ imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
+ type: IntegrationType.BWDC,
+ },
+ {
+ name: "Splunk",
+ linkURL: "https://bitwarden.com/help/splunk-siem/",
+ image: "../../../../../../../images/integrations/logo-splunk-black.svg",
+ imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
+ type: IntegrationType.EVENT,
+ },
+ {
+ name: "Microsoft Sentinel",
+ linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
+ image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
+ type: IntegrationType.EVENT,
+ },
+ {
+ name: "Rapid7",
+ linkURL: "https://bitwarden.com/help/rapid7-siem/",
+ image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
+ imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
+ type: IntegrationType.EVENT,
+ },
+ {
+ name: "Elastic",
+ linkURL: "https://bitwarden.com/help/elastic-siem/",
+ image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
+ type: IntegrationType.EVENT,
+ },
+ {
+ name: "Panther",
+ linkURL: "https://bitwarden.com/help/panther-siem/",
+ image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
+ type: IntegrationType.EVENT,
+ },
+ {
+ name: "Sumo Logic",
+ linkURL: "https://bitwarden.com/help/sumo-logic-siem/",
+ image: "../../../../../../../images/integrations/logo-sumo-logic-siem.svg",
+ imageDarkMode: "../../../../../../../images/integrations/logo-sumo-logic-siem-darkmode.svg",
+ type: IntegrationType.EVENT,
+ newBadgeExpiration: "2025-12-31",
+ },
+ {
+ name: "Microsoft Intune",
+ linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
+ image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
+ type: IntegrationType.DEVICE,
+ },
+ ];
+
+ const featureEnabled = await firstValueFrom(
+ this.configService.getFeatureFlag$(FeatureFlag.EventManagementForDataDogAndCrowdStrike),
+ );
+
+ if (featureEnabled) {
+ integrations.push(
+ {
+ name: OrganizationIntegrationServiceName.CrowdStrike,
+ linkURL: "https://bitwarden.com/help/crowdstrike-siem/",
+ image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
+ type: IntegrationType.EVENT,
+ canSetupConnection: true,
+ integrationType: OrganizationIntegrationType.Hec,
+ },
+ {
+ name: OrganizationIntegrationServiceName.Datadog,
+ linkURL: "https://bitwarden.com/help/datadog-siem/",
+ image: "../../../../../../../images/integrations/logo-datadog-color.svg",
+ type: IntegrationType.EVENT,
+ canSetupConnection: true,
+ integrationType: OrganizationIntegrationType.Datadog,
+ },
+ );
+ }
+
+ // Add Huntress SIEM integration (separate feature flag)
+ const huntressFeatureEnabled = await firstValueFrom(
+ this.configService.getFeatureFlag$(FeatureFlag.EventManagementForHuntress),
+ );
+
+ if (huntressFeatureEnabled) {
+ integrations.push({
+ name: OrganizationIntegrationServiceName.Huntress,
+ linkURL: "https://bitwarden.com/help/huntress-siem/",
+ image: "../../../../../../../images/integrations/logo-huntress-siem.svg",
+ type: IntegrationType.EVENT,
+ description: "huntressEventIntegrationDesc",
+ canSetupConnection: true,
+ integrationType: OrganizationIntegrationType.Hec,
+ });
+ }
+
+ const orgIntegrations = await firstValueFrom(
+ this.organizationIntegrationService.integrations$.pipe(take(1)),
+ );
+
+ const merged = integrations.map((i) => ({
+ ...i,
+ organizationIntegration: orgIntegrations.find((o) => o.serviceName === i.name) ?? null,
+ }));
+
+ this.state.setIntegrations(merged);
+
+ return true;
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts
new file mode 100644
index 00000000000..5e7e6a78ba4
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/organization-integrations.state.ts
@@ -0,0 +1,22 @@
+import { Injectable, signal } from "@angular/core";
+
+import { Integration } from "@bitwarden/bit-common/dirt/organization-integrations/models/integration";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+
+@Injectable()
+export class OrganizationIntegrationsState {
+ private readonly _integrations = signal([]);
+ private readonly _organization = signal(undefined);
+
+ // Signals
+ integrations = this._integrations.asReadonly();
+ organization = this._organization.asReadonly();
+
+ setOrganization(val: Organization | null) {
+ this._organization.set(val ?? undefined);
+ }
+
+ setIntegrations(val: Integration[]) {
+ this._integrations.set(val);
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html
new file mode 100644
index 00000000000..ca5ed9ee30c
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.html
@@ -0,0 +1,12 @@
+@let integrationsList = integrations();
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts
new file mode 100644
index 00000000000..d0d2a1666f2
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/single-sign-on/single-sign-on.component.ts
@@ -0,0 +1,22 @@
+import { Component } from "@angular/core";
+
+import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
+import { FilterIntegrationsPipe } from "../integrations.pipe";
+import { OrganizationIntegrationsState } from "../organization-integrations.state";
+
+// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
+// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
+@Component({
+ selector: "single-sign-on",
+ templateUrl: "single-sign-on.component.html",
+ imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
+})
+export class SingleSignOnComponent {
+ integrations = this.state.integrations;
+ IntegrationType = IntegrationType;
+
+ constructor(private state: OrganizationIntegrationsState) {}
+}
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html
new file mode 100644
index 00000000000..a254f334e21
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.html
@@ -0,0 +1,25 @@
+@let org = organization();
+@let integrationsList = integrations();
+
+
+
+ {{ "scimIntegration" | i18n }}
+
+
+ {{ "scimIntegrationDescStart" | i18n }}
+ {{ "scimIntegration" | i18n }}
+ {{ "scimIntegrationDescEnd" | i18n }}
+
+
+
+
+
+ {{ "bwdc" | i18n }}
+
+ {{ "bwdcDesc" | i18n }}
+
+
diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts
new file mode 100644
index 00000000000..f484674d224
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/user-provisioning/user-provisioning.component.ts
@@ -0,0 +1,26 @@
+import { Component } from "@angular/core";
+
+import { IntegrationType } from "@bitwarden/common/enums/integration-type.enum";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
+import { FilterIntegrationsPipe } from "../integrations.pipe";
+import { OrganizationIntegrationsState } from "../organization-integrations.state";
+
+// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
+// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
+@Component({
+ selector: "user-provisioning",
+ templateUrl: "user-provisioning.component.html",
+ imports: [SharedModule, IntegrationGridComponent, FilterIntegrationsPipe],
+})
+export class UserProvisioningComponent {
+ organization = this.state.organization;
+ integrations = this.state.integrations;
+
+ constructor(private state: OrganizationIntegrationsState) {}
+
+ get IntegrationType(): typeof IntegrationType {
+ return IntegrationType;
+ }
+}
diff --git a/libs/common/src/platform/services/fido2/domain-utils.spec.ts b/libs/common/src/platform/services/fido2/domain-utils.spec.ts
index 4b99c06cdec..284555052dd 100644
--- a/libs/common/src/platform/services/fido2/domain-utils.spec.ts
+++ b/libs/common/src/platform/services/fido2/domain-utils.spec.ts
@@ -2,6 +2,18 @@ import { isValidRpId } from "./domain-utils";
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm.
describe("validateRpId", () => {
+ it("should not be valid when rpId is null", () => {
+ const origin = "example.com";
+
+ expect(isValidRpId(null, origin)).toBe(false);
+ });
+
+ it("should not be valid when origin is null", () => {
+ const rpId = "example.com";
+
+ expect(isValidRpId(rpId, null)).toBe(false);
+ });
+
it("should not be valid when rpId is more specific than origin", () => {
const rpId = "sub.login.bitwarden.com";
const origin = "https://login.bitwarden.com:1337";
@@ -25,7 +37,7 @@ describe("validateRpId", () => {
it("should not be valid when rpId and origin are both different TLD", () => {
const rpId = "bitwarden";
- const origin = "localhost";
+ const origin = "https://localhost";
expect(isValidRpId(rpId, origin)).toBe(false);
});
@@ -34,14 +46,14 @@ describe("validateRpId", () => {
// adding support for ip-addresses and other TLDs
it("should not be valid when rpId and origin are both the same TLD", () => {
const rpId = "bitwarden";
- const origin = "bitwarden";
+ const origin = "https://bitwarden";
expect(isValidRpId(rpId, origin)).toBe(false);
});
it("should not be valid when rpId and origin are ip-addresses", () => {
const rpId = "127.0.0.1";
- const origin = "127.0.0.1";
+ const origin = "https://127.0.0.1";
expect(isValidRpId(rpId, origin)).toBe(false);
});
@@ -80,4 +92,11 @@ describe("validateRpId", () => {
expect(isValidRpId(rpId, origin)).toBe(true);
});
+
+ it("should not be valid for a partial match of a subdomain", () => {
+ const rpId = "accounts.example.com";
+ const origin = "https://evilaccounts.example.com";
+
+ expect(isValidRpId(rpId, origin)).toBe(false);
+ });
});
diff --git a/libs/common/src/platform/services/fido2/domain-utils.ts b/libs/common/src/platform/services/fido2/domain-utils.ts
index 67874355908..542beae3435 100644
--- a/libs/common/src/platform/services/fido2/domain-utils.ts
+++ b/libs/common/src/platform/services/fido2/domain-utils.ts
@@ -1,17 +1,78 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { parse } from "tldts";
+/**
+ * Validates whether a Relying Party ID (rpId) is valid for a given origin according to WebAuthn specifications.
+ *
+ * The validation enforces the following rules:
+ * - The origin must use the HTTPS scheme
+ * - Both rpId and origin must be valid domain names (not IP addresses)
+ * - Both must have the same registrable domain (e.g., example.com)
+ * - The origin must either exactly match the rpId or be a subdomain of it
+ * - Single-label domains are rejected unless they are 'localhost'
+ * - Localhost is always valid when both rpId and origin are localhost
+ *
+ * @param rpId - The Relying Party identifier to validate
+ * @param origin - The origin URL to validate against (must start with https://)
+ * @returns `true` if the rpId is valid for the given origin, `false` otherwise
+ *
+ */
export function isValidRpId(rpId: string, origin: string) {
+ if (!rpId || !origin) {
+ return false;
+ }
+
const parsedOrigin = parse(origin, { allowPrivateDomains: true });
const parsedRpId = parse(rpId, { allowPrivateDomains: true });
- return (
- (parsedOrigin.domain == null &&
- parsedOrigin.hostname == parsedRpId.hostname &&
- parsedOrigin.hostname == "localhost") ||
- (parsedOrigin.domain != null &&
- parsedOrigin.domain == parsedRpId.domain &&
- parsedOrigin.subdomain.endsWith(parsedRpId.subdomain))
- );
+ if (!parsedRpId || !parsedOrigin) {
+ return false;
+ }
+
+ // Special case: localhost is always valid when both match
+ if (parsedRpId.hostname === "localhost" && parsedOrigin.hostname === "localhost") {
+ return true;
+ }
+
+ // The origin's scheme must be https.
+ if (!origin.startsWith("https://")) {
+ return false;
+ }
+
+ // Reject IP addresses (both must be domain names)
+ if (parsedRpId.isIp || parsedOrigin.isIp) {
+ return false;
+ }
+
+ // Reject single-label domains (TLDs) unless it's localhost
+ // This ensures we have proper domains like "example.com" not just "example"
+ if (rpId !== "localhost" && !rpId.includes(".")) {
+ return false;
+ }
+
+ if (
+ parsedOrigin.hostname != null &&
+ parsedOrigin.hostname !== "localhost" &&
+ !parsedOrigin.hostname.includes(".")
+ ) {
+ return false;
+ }
+
+ // The registrable domains must match
+ // This ensures a.example.com and b.example.com share base domain
+ if (parsedRpId.domain !== parsedOrigin.domain) {
+ return false;
+ }
+
+ // Check exact match
+ if (parsedOrigin.hostname === rpId) {
+ return true;
+ }
+
+ // Check if origin is a subdomain of rpId
+ // This prevents "evilaccounts.example.com" from matching "accounts.example.com"
+ if (parsedOrigin.hostname != null && parsedOrigin.hostname.endsWith("." + rpId)) {
+ return true;
+ }
+
+ return false;
}
diff --git a/libs/common/src/tools/providers.spec.ts b/libs/common/src/tools/providers.spec.ts
index d457b1df85e..5953e5ebab2 100644
--- a/libs/common/src/tools/providers.spec.ts
+++ b/libs/common/src/tools/providers.spec.ts
@@ -4,7 +4,6 @@ import { PolicyService } from "../admin-console/abstractions/policy/policy.servi
import { ConfigService } from "../platform/abstractions/config/config.service";
import { LogService } from "../platform/abstractions/log.service";
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
-import { SdkService } from "../platform/abstractions/sdk/sdk.service";
import { StateProvider } from "../platform/state";
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
@@ -21,7 +20,6 @@ describe("SystemServiceProvider", () => {
let mockLogger: LogService;
let mockEnvironment: MockProxy;
let mockConfigService: ConfigService;
- let mockSdkService: SdkService;
beforeEach(() => {
jest.resetAllMocks();
@@ -33,7 +31,6 @@ describe("SystemServiceProvider", () => {
mockLogger = mock();
mockEnvironment = mock();
mockConfigService = mock();
- mockSdkService = mock();
});
describe("createSystemServiceProvider", () => {
@@ -48,7 +45,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result).toHaveProperty("policy", mockPolicy);
@@ -70,7 +66,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result.extension).toBeInstanceOf(ExtensionService);
@@ -88,7 +83,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
@@ -108,7 +102,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
@@ -128,7 +121,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result.extension).toBeInstanceOf(ExtensionService);
@@ -146,7 +138,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result.policy).toBe(mockPolicy);
@@ -163,7 +154,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result.configService).toBe(mockConfigService);
@@ -180,7 +170,6 @@ describe("SystemServiceProvider", () => {
mockLogger,
mockEnvironment,
mockConfigService,
- mockSdkService,
);
expect(result.environment).toBe(mockEnvironment);
diff --git a/libs/common/src/tools/providers.ts b/libs/common/src/tools/providers.ts
index b1621f19c21..ac42c556042 100644
--- a/libs/common/src/tools/providers.ts
+++ b/libs/common/src/tools/providers.ts
@@ -1,10 +1,10 @@
import { LogService } from "@bitwarden/logging";
+import { BitwardenClient } from "@bitwarden/sdk-internal";
import { StateProvider } from "@bitwarden/state";
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigService } from "../platform/abstractions/config/config.service";
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
-import { SdkService } from "../platform/abstractions/sdk/sdk.service";
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
import { ExtensionRegistry } from "./extension/extension-registry.abstraction";
@@ -29,7 +29,7 @@ export type SystemServiceProvider = {
readonly environment: PlatformUtilsService;
/** SDK Service */
- readonly sdk: SdkService;
+ readonly sdk?: BitwardenClient;
};
/** Constructs a system service provider. */
@@ -41,7 +41,6 @@ export function createSystemServiceProvider(
logger: LogService,
environment: PlatformUtilsService,
configService: ConfigService,
- sdk: SdkService,
): SystemServiceProvider {
let log: LogProvider;
if (environment.isDev()) {
@@ -63,6 +62,5 @@ export function createSystemServiceProvider(
log,
configService,
environment,
- sdk,
};
}
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index 523a6490fb8..6373a511724 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -934,12 +934,17 @@ export class CipherService implements CipherServiceAbstraction {
userId: UserId,
orgAdmin?: boolean,
): Promise {
+ // Clear the cache before creating the cipher. The SDK internally updates the encrypted storage
+ // but the timing of the storage emitting the new values differs across platforms. Clearing the cache after
+ // `createWithServer` can cause race conditions where the cache is cleared after the
+ // encrypted storage has already been updated and thus downstream consumers not getting updated data.
+ await this.clearCache(userId);
+
const resultCipherView = await this.cipherSdkService.createWithServer(
cipherView,
userId,
orgAdmin,
);
- await this.clearCache(userId);
return resultCipherView;
}
@@ -993,13 +998,18 @@ export class CipherService implements CipherServiceAbstraction {
originalCipherView?: CipherView,
orgAdmin?: boolean,
): Promise {
+ // Clear the cache before updating the cipher. The SDK internally updates the encrypted storage
+ // but the timing of the storage emitting the new values differs across platforms. Clearing the cache after
+ // `updateWithServer` can cause race conditions where the cache is cleared after the
+ // encrypted storage has already been updated and thus downstream consumers not getting updated data.
+ await this.clearCache(userId);
+
const resultCipherView = await this.cipherSdkService.updateWithServer(
cipher,
userId,
originalCipherView,
orgAdmin,
);
- await this.clearCache(userId);
return resultCipherView;
}
diff --git a/libs/eslint/components/no-bwi-class-usage.mjs b/libs/eslint/components/no-bwi-class-usage.mjs
index 8260587ce45..6f856646a07 100644
--- a/libs/eslint/components/no-bwi-class-usage.mjs
+++ b/libs/eslint/components/no-bwi-class-usage.mjs
@@ -1,6 +1,21 @@
export const errorMessage =
"Use component instead of applying 'bwi' classes directly. Example: ";
+// Helper classes from libs/angular/src/scss/bwicons/styles/style.scss
+// These are utility classes that can be used independently
+const ALLOWED_BWI_HELPER_CLASSES = new Set([
+ "bwi-fw", // Fixed width
+ "bwi-sm", // Small
+ "bwi-lg", // Large
+ "bwi-2x", // 2x size
+ "bwi-3x", // 3x size
+ "bwi-4x", // 4x size
+ "bwi-spin", // Spin animation
+ "bwi-ul", // List
+ "bwi-li", // List item
+ "bwi-rotate-270", // Rotation
+]);
+
export default {
meta: {
type: "suggestion",
@@ -25,12 +40,23 @@ export default {
for (const classAttr of classAttrs) {
const classValue = classAttr.value || "";
- // Check if the class value contains 'bwi' or 'bwi-'
- // This handles both string literals and template expressions
- const hasBwiClass =
- typeof classValue === "string" && /\bbwi(?:-[\w-]+)?\b/.test(classValue);
+ if (typeof classValue !== "string") {
+ continue;
+ }
- if (hasBwiClass) {
+ // Extract all bwi classes from the class string
+ const bwiClassMatches = classValue.match(/\bbwi(?:-[\w-]+)?\b/g);
+
+ if (!bwiClassMatches) {
+ continue;
+ }
+
+ // Check if any bwi class is NOT in the allowed helper classes list
+ const hasDisallowedBwiClass = bwiClassMatches.some(
+ (cls) => !ALLOWED_BWI_HELPER_CLASSES.has(cls),
+ );
+
+ if (hasDisallowedBwiClass) {
context.report({
node,
message: errorMessage,
diff --git a/libs/eslint/components/no-bwi-class-usage.spec.mjs b/libs/eslint/components/no-bwi-class-usage.spec.mjs
index abb5ebe3b29..768081ac966 100644
--- a/libs/eslint/components/no-bwi-class-usage.spec.mjs
+++ b/libs/eslint/components/no-bwi-class-usage.spec.mjs
@@ -14,10 +14,42 @@ ruleTester.run("no-bwi-class-usage", rule.default, {
name: "should allow bit-icon component usage",
code: ``,
},
+ {
+ name: "should allow bit-icon with bwi-fw helper class",
+ code: ``,
+ },
+ {
+ name: "should allow bit-icon with name attribute and bwi-fw helper class",
+ code: ``,
+ },
{
name: "should allow elements without bwi classes",
code: ``,
},
+ {
+ name: "should allow bwi-fw helper class alone",
+ code: ``,
+ },
+ {
+ name: "should allow bwi-sm helper class",
+ code: ``,
+ },
+ {
+ name: "should allow multiple helper classes together",
+ code: ``,
+ },
+ {
+ name: "should allow helper classes with other non-bwi classes",
+ code: ``,
+ },
+ {
+ name: "should allow bwi-spin helper class",
+ code: ``,
+ },
+ {
+ name: "should allow bwi-rotate-270 helper class",
+ code: ``,
+ },
],
invalid: [
{
@@ -31,14 +63,19 @@ ruleTester.run("no-bwi-class-usage", rule.default, {
errors: [{ message: errorMessage }],
},
{
- name: "should error on single bwi-* class",
+ name: "should error on single bwi-* icon class",
code: ``,
errors: [{ message: errorMessage }],
},
{
- name: "should error on bwi-fw modifier",
+ name: "should error on icon classes even with helper classes",
code: ``,
errors: [{ message: errorMessage }],
},
+ {
+ name: "should error on base bwi class alone",
+ code: ``,
+ errors: [{ message: errorMessage }],
+ },
],
});
diff --git a/libs/importer/src/components/importer-providers.ts b/libs/importer/src/components/importer-providers.ts
index eb7e58e9259..18c148ebe2e 100644
--- a/libs/importer/src/components/importer-providers.ts
+++ b/libs/importer/src/components/importer-providers.ts
@@ -13,7 +13,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction";
@@ -72,7 +71,6 @@ export const ImporterProviders: SafeProvider[] = [
LogService,
PlatformUtilsService,
ConfigService,
- SdkService,
],
}),
safeProvider({
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html
index f41375edd5a..6f2fcbeafcf 100644
--- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html
+++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html
@@ -34,9 +34,11 @@
{{ "fileFormat" | i18n }}
-
-
-
+
diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts
index 39d0dd298a2..935f7dc2d60 100644
--- a/libs/tools/generator/components/src/generator-services.module.ts
+++ b/libs/tools/generator/components/src/generator-services.module.ts
@@ -1,10 +1,12 @@
import { NgModule } from "@angular/core";
+import { from, take } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -124,7 +126,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken (featureFlag = ff));
const metadata = new providers.GeneratorMetadataProvider(
userStateDeps,
system,
Object.values(BuiltIn),
);
+ const sdkService = featureFlag ? system.sdk : undefined;
const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy);
const generator: providers.GeneratorDependencyProvider = {
randomizer: random,
client: new RestClient(api, i18n),
i18nService: i18n,
- sdk: system.sdk,
+ sdk: sdkService,
now: Date.now,
};
diff --git a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts
index 09c7d62b1ad..03be21eeefb 100644
--- a/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts
+++ b/libs/tools/generator/core/src/engine/sdk-password-randomizer.ts
@@ -1,6 +1,3 @@
-import { firstValueFrom } from "rxjs";
-
-import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import {
BitwardenClient,
PassphraseGeneratorRequest,
@@ -23,11 +20,11 @@ export class SdkPasswordRandomizer
CredentialGenerator
{
/** Instantiates the password randomizer
- * @param service access to SDK client to call upon password/passphrase generation
+ * @param client access to SDK client to call upon password/passphrase generation
* @param currentTime gets the current datetime in epoch time
*/
constructor(
- private service: SdkService,
+ private client: BitwardenClient,
private currentTime: () => number,
) {}
@@ -43,9 +40,8 @@ export class SdkPasswordRandomizer
request: GenerateRequest,
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
) {
- const sdk: BitwardenClient = await firstValueFrom(this.service.client$);
if (isPasswordGenerationOptions(settings)) {
- const password = await sdk.generator().password(convertPasswordRequest(settings));
+ const password = await this.client.generator().password(convertPasswordRequest(settings));
return new GeneratedCredential(
password,
@@ -55,7 +51,9 @@ export class SdkPasswordRandomizer
request.website,
);
} else if (isPassphraseGenerationOptions(settings)) {
- const passphrase = await sdk.generator().passphrase(convertPassphraseRequest(settings));
+ const passphrase = await this.client
+ .generator()
+ .passphrase(convertPassphraseRequest(settings));
return new GeneratedCredential(
passphrase,
diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts
index 015cc25a8ec..bdf021c50f3 100644
--- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts
+++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts
@@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
-import { SdkPasswordRandomizer } from "../../engine";
+import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PassphraseGenerationOptions } from "../../types";
@@ -22,6 +22,16 @@ describe("password - eff words generator metadata", () => {
});
});
+ describe("engine.create", () => {
+ const nonSdkDependencyProvider = mock();
+ nonSdkDependencyProvider.sdk = undefined;
+ it("returns a password randomizer", () => {
+ expect(effPassphrase.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(
+ PasswordRandomizer,
+ );
+ });
+ });
+
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata | null = null;
beforeEach(() => {
diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts
index d6d78c83293..fc96ce46c2b 100644
--- a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts
+++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts
@@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
-import { SdkPasswordRandomizer } from "../../engine";
+import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
@@ -30,6 +30,9 @@ const passphrase: GeneratorMetadata = {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator {
+ if (dependencies.sdk == undefined) {
+ return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
+ }
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},
diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts
index d066b9f1597..9efd5350c21 100644
--- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts
+++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts
@@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
-import { SdkPasswordRandomizer } from "../../engine";
+import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { PasswordGenerationOptions } from "../../types";
@@ -22,6 +22,14 @@ describe("password - characters generator metadata", () => {
});
});
+ describe("engine.create", () => {
+ const nonSdkDependencyProvider = mock();
+ nonSdkDependencyProvider.sdk = undefined;
+ it("returns a password randomizer", () => {
+ expect(password.engine.create(nonSdkDependencyProvider)).toBeInstanceOf(PasswordRandomizer);
+ });
+ });
+
describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata = null!;
beforeEach(() => {
diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts
index d25ea1e8f46..721be8dc3f0 100644
--- a/libs/tools/generator/core/src/metadata/password/random-password.ts
+++ b/libs/tools/generator/core/src/metadata/password/random-password.ts
@@ -3,7 +3,7 @@ import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { deepFreeze } from "@bitwarden/common/tools/util";
-import { SdkPasswordRandomizer } from "../../engine";
+import { PasswordRandomizer, SdkPasswordRandomizer } from "../../engine";
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
import { GeneratorDependencyProvider } from "../../providers";
import { CredentialGenerator, PasswordGeneratorSettings } from "../../types";
@@ -30,6 +30,9 @@ const password: GeneratorMetadata = deepFreeze({
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator {
+ if (dependencies.sdk == undefined) {
+ return new PasswordRandomizer(dependencies.randomizer, dependencies.now);
+ }
return new SdkPasswordRandomizer(dependencies.sdk, dependencies.now);
},
},
diff --git a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts
index 8700bbc8a24..a6dbbeaa537 100644
--- a/libs/tools/generator/core/src/providers/generator-dependency-provider.ts
+++ b/libs/tools/generator/core/src/providers/generator-dependency-provider.ts
@@ -1,6 +1,6 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
+import { BitwardenClient } from "@bitwarden/sdk-internal";
import { Randomizer } from "../abstractions";
@@ -10,6 +10,6 @@ export type GeneratorDependencyProvider = {
// FIXME: introduce `I18nKeyOrLiteral` into forwarder
// structures and remove this dependency
i18nService: I18nService;
- sdk: SdkService;
+ sdk?: BitwardenClient;
now: () => number;
};
diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts
index f79bb986325..39ff74ad901 100644
--- a/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts
+++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.spec.ts
@@ -5,6 +5,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
import {
@@ -95,6 +96,8 @@ const SomePolicyService = mock();
const SomeExtensionService = mock();
+const SomeConfigService = mock;
+
const SomeSdkService = mock;
const ApplicationProvider = {
@@ -107,6 +110,9 @@ const ApplicationProvider = {
/** Event monitoring and diagnostic interfaces */
log: disabledSemanticLoggerProvider,
+ /** Feature flag retrieval */
+ configService: SomeConfigService,
+
/** SDK access for password generation */
sdk: SomeSdkService,
} as unknown as SystemServiceProvider;
diff --git a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html
index fb9b82c44e5..7b966bb0345 100644
--- a/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html
+++ b/libs/tools/send/send-ui/src/new-send-dropdown/new-send-dropdown.component.html
@@ -7,11 +7,13 @@
{{ "sendTypeText" | i18n }}
-
+
{{ "sendTypeFile" | i18n }}
+ {{ "popOutNewWindow" | i18n }}
+
diff --git a/libs/tools/send/send-ui/src/send-search/send-search.component.html b/libs/tools/send/send-ui/src/send-search/send-search.component.html
index 7cf154c0ee8..fbbe436d158 100644
--- a/libs/tools/send/send-ui/src/send-search/send-search.component.html
+++ b/libs/tools/send/send-ui/src/send-search/send-search.component.html
@@ -1,7 +1 @@
-
-
+
diff --git a/libs/tools/send/send-ui/src/send-search/send-search.component.ts b/libs/tools/send/send-ui/src/send-search/send-search.component.ts
index 02cb5ef2eda..03eaf9b3430 100644
--- a/libs/tools/send/send-ui/src/send-search/send-search.component.ts
+++ b/libs/tools/send/send-ui/src/send-search/send-search.component.ts
@@ -1,50 +1,55 @@
-import { CommonModule } from "@angular/common";
-import { Component } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { ChangeDetectionStrategy, Component, inject, model } from "@angular/core";
+import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
-import { Subject, Subscription, debounceTime, filter } from "rxjs";
+import { debounceTime, filter } from "rxjs";
-import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchModule } from "@bitwarden/components";
+import { I18nPipe } from "@bitwarden/ui-common";
import { SendItemsService } from "../services/send-items.service";
const SearchTextDebounceInterval = 200;
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
+/**
+ * Search component for filtering Send items.
+ *
+ * Provides a search input that filters the Send list with debounced updates.
+ * Syncs with the service's latest search text to maintain state across navigation.
+ */
@Component({
- imports: [CommonModule, SearchModule, JslibModule, FormsModule],
selector: "tools-send-search",
templateUrl: "send-search.component.html",
+ imports: [FormsModule, I18nPipe, SearchModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendSearchComponent {
- searchText: string = "";
+ private sendListItemService = inject(SendItemsService);
- private searchText$ = new Subject();
+ /** The current search text entered by the user. */
+ protected readonly searchText = model("");
- constructor(private sendListItemService: SendItemsService) {
+ constructor() {
this.subscribeToLatestSearchText();
this.subscribeToApplyFilter();
}
- onSearchTextChanged() {
- this.searchText$.next(this.searchText);
- }
-
- subscribeToLatestSearchText(): Subscription {
- return this.sendListItemService.latestSearchText$
+ private subscribeToLatestSearchText(): void {
+ this.sendListItemService.latestSearchText$
.pipe(
takeUntilDestroyed(),
filter((data) => !!data),
)
.subscribe((text) => {
- this.searchText = text;
+ this.searchText.set(text);
});
}
- subscribeToApplyFilter(): Subscription {
- return this.searchText$
+ /**
+ * Applies the search filter to the Send list with a debounce delay.
+ * This prevents excessive filtering while the user is still typing.
+ */
+ private subscribeToApplyFilter(): void {
+ toObservable(this.searchText)
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
.subscribe((data) => {
this.sendListItemService.applyFilter(data);
diff --git a/libs/tools/send/send-ui/src/send-table/send-table.component.html b/libs/tools/send/send-ui/src/send-table/send-table.component.html
index cc2fca2c41c..1c235415cae 100644
--- a/libs/tools/send/send-ui/src/send-table/send-table.component.html
+++ b/libs/tools/send/send-ui/src/send-table/send-table.component.html
@@ -15,10 +15,10 @@
@if (s.type == sendType.File) {
-
+
}
@if (s.type == sendType.Text) {
-
+
}