diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b1203230688..4e38a181b8c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3903,6 +3903,9 @@ "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." }, + "generatingRiskInsights": { + "message": "Generating your risk insights..." + }, "freeTrialEndPromptCount": { "message": "Your free trial ends in $COUNT$ days.", "placeholders": { diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts new file mode 100644 index 00000000000..3a0ff4176eb --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { shareReplay } from "rxjs/operators"; + +import { ApplicationHealthReportDetail } from "../models/password-health"; + +import { RiskInsightsReportService } from "./risk-insights-report.service"; + +/** + * Singleton service to manage the report details for the Risk Insights reports. + */ +@Injectable({ + providedIn: "root", +}) +export class RiskInsightsDataService { + // Map to store observables per organizationId + private applicationsReportMap = new Map>(); + + constructor(private reportService: RiskInsightsReportService) {} + + /** + * Returns an observable for the applications report of a given organizationId. + * Utilizes shareReplay to ensure that the data is fetched only once + * and shared among multiple subscribers. + * @param organizationId The ID of the organization. + * @returns Observable of ApplicationHealthReportDetail[]. + */ + getApplicationsReport$(organizationId: string): Observable { + // If the observable for this organizationId already exists, return it + if (this.applicationsReportMap.has(organizationId)) { + return this.applicationsReportMap.get(organizationId)!; + } + + const applicationsReport$ = this.reportService + .generateApplicationsReport$(organizationId) + .pipe(shareReplay({ bufferSize: 1, refCount: true })); + + // Store the observable in the map for future subscribers + this.applicationsReportMap.set(organizationId, applicationsReport$); + + return applicationsReport$; + } + + /** + * Clears the cached observable for a specific organizationId. + * @param organizationId The ID of the organization. + */ + clearApplicationsReportCache(organizationId: string): void { + if (this.applicationsReportMap.has(organizationId)) { + this.applicationsReportMap.delete(organizationId); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index 4db1e2f5e20..fd1a3b0b84c 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -20,6 +20,7 @@ import { MaximumVaultTimeoutPolicyComponent } from "./admin-console/policies/max import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free-families-sponsorship.component"; +import { AccessIntelligenceModule } from "./tools/access-intelligence/access-intelligence.module"; /** * This is the AppModule for the commercial version of Bitwarden. @@ -41,6 +42,7 @@ import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free- AppRoutingModule, OssRoutingModule, OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly + AccessIntelligenceModule, RouterModule, WildcardRoutingModule, // Needs to be last to catch all non-existing routes ], diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 3f177119aa8..87b75dee70c 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -1,9 +1,33 @@ import { NgModule } from "@angular/core"; +import { + MemberCipherDetailsApiService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights/services"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { RiskInsightsComponent } from "./risk-insights.component"; @NgModule({ imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule], + providers: [ + { + provide: MemberCipherDetailsApiService, + deps: [ApiService], + }, + { + provide: RiskInsightsReportService, + deps: [ + PasswordStrengthServiceAbstraction, + AuditService, + CipherService, + MemberCipherDetailsApiService, + ], + }, + ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index 4ed31adea78..08595f4e152 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -1,12 +1,7 @@
- - {{ "loading" | i18n }} +
-
+

@@ -34,15 +29,15 @@

@@ -57,7 +52,7 @@ type="button" buttonType="secondary" bitButton - *ngIf="isCritialAppsFeatureEnabled" + *ngIf="isCritialAppsFeatureEnabled$ | async" [disabled]="!selectedIds.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" @@ -69,7 +64,7 @@ - + {{ "application" | i18n }} {{ "atRiskPasswords" | i18n }} {{ "totalPasswords" | i18n }} @@ -79,34 +74,34 @@ - + - {{ r.name }} + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberDetails.length }} - {{ r.totalMembers }} + {{ r.memberCount }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts index 6ee2ecf1690..d5b1d6b136b 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts @@ -1,18 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, firstValueFrom, map } from "rxjs"; +import { combineLatest, debounceTime, map, Observable, of, switchMap, tap } from "rxjs"; +import { + MemberCipherDetailsApiService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetail, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -27,52 +32,73 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; +import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; @Component({ standalone: true, selector: "tools-all-applications", templateUrl: "./all-applications.component.html", - imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule], + imports: [ + ApplicationsLoadingComponent, + HeaderModule, + CardComponent, + SearchModule, + PipesModule, + NoItemsModule, + SharedModule, + ], + providers: [MemberCipherDetailsApiService, RiskInsightsReportService], }) export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); - protected loading = false; + protected loading = true; protected organization: Organization; noItemsIcon = Icons.Security; protected markingAsCritical = false; - isCritialAppsFeatureEnabled = false; + protected applicationSummary: ApplicationHealthReportSummary; - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; + isCritialAppsFeatureEnabled$: Observable; - async ngOnInit() { - this.activatedRoute.paramMap + ngOnInit() { + // Combine route parameters and feature flag + combineLatest([ + this.activatedRoute.paramMap.pipe( + switchMap((params) => { + const organizationId = params.get("organizationId"); + if (!organizationId) { + this.loading = false; + return of(null); + } + return this.organizationService.get$(organizationId).pipe( + tap((org) => (this.organization = org)), + switchMap(() => + this.riskInsightsReportService.generateApplicationsReport$(organizationId), + ), + tap((applicationsReport) => { + this.dataSource.data = applicationsReport; + this.applicationSummary = + this.riskInsightsReportService.generateApplicationsSummary(applicationsReport); + this.loading = false; + }), + ); + }), + ), + this.configService.getFeatureFlag$(FeatureFlag.CriticalApps).pipe(), + ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); - // TODO: use organizationId to fetch data - }), + map(([_, featureFlag]) => featureFlag), + tap((flag) => (this.isCritialAppsFeatureEnabled$ = of(flag))), ) .subscribe(); - - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( - FeatureFlag.CriticalApps, - ); } constructor( protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected riskInsightsReportService: RiskInsightsReportService, protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, @@ -80,7 +106,6 @@ export class AllApplicationsComponent implements OnInit { protected organizationService: OrganizationService, protected configService: ConfigService, ) { - this.dataSource.data = applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); @@ -95,22 +120,25 @@ export class AllApplicationsComponent implements OnInit { }); }; - markAppsAsCritical = async () => { + markAppsAsCritical() { // TODO: Send to API once implemented this.markingAsCritical = true; - return new Promise((resolve) => { - setTimeout(() => { - this.selectedIds.clear(); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("appsMarkedAsCritical"), - }); - resolve(true); - this.markingAsCritical = false; - }, 1000); - }); - }; + of(true) + .pipe( + debounceTime(1000), // Simulate delay + tap(() => { + this.selectedIds.clear(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("appsMarkedAsCritical"), + }); + this.markingAsCritical = false; + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + } trackByFunction(_: number, item: CipherView) { return item.id; diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html new file mode 100644 index 00000000000..22ba8cc37ce --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -0,0 +1,8 @@ +
+ +

{{ "generatingRiskInsights" | i18n }}

+
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts new file mode 100644 index 00000000000..1cafa62c608 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +@Component({ + selector: "tools-risk-insights-loading", + standalone: true, + imports: [CommonModule, JslibModule], + templateUrl: "./risk-insights-loading.component.html", +}) +export class ApplicationsLoadingComponent { + constructor() {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 6df47e3c46f..8c43abadff0 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -2,7 +2,7 @@

{{ "riskInsights" | i18n }}

{{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }} +
@@ -29,7 +29,7 @@ - +