1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

PM-20577 Retrieve data from cache

This commit is contained in:
voommen-livefront
2025-05-13 13:25:37 -05:00
parent bc019ce14d
commit c2719b491e
7 changed files with 135 additions and 133 deletions

View File

@@ -183,7 +183,7 @@ export interface SaveRiskInsightsReportResponse {
export interface GetRiskInsightsReportResponse {
id: string;
organizationId: OrganizationId;
date: string;
reportDate: string;
reportData: string;
totalMembers: number;
totalAtRiskMembers: number;

View File

@@ -49,10 +49,6 @@ export class CriticalAppsService {
// Get a list of critical apps for a given organization
getAppsListForOrg(orgId: string): Observable<PasswordHealthReportApplicationsResponse[]> {
if (this.criticalAppsList.value.length === 0) {
return of([]);
}
return this.criticalAppsList
.asObservable()
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));

View File

@@ -1,19 +1,27 @@
import { BehaviorSubject } from "rxjs";
import { finalize } from "rxjs/operators";
import { finalize, switchMap } from "rxjs/operators";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
AppAtRiskMembersDialogParams,
ApplicationHealthReportDetail,
ApplicationHealthReportSummary,
AtRiskApplicationDetail,
AtRiskMemberDetail,
DrawerType,
} from "../models/password-health";
import { RiskInsightsApiService } from "./risk-insights-api.service";
import { RiskInsightsReportService } from "./risk-insights-report.service";
export class RiskInsightsDataService {
private applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(null);
private appsSummarySubject = new BehaviorSubject<ApplicationHealthReportSummary | null>(null);
private isReportFromArchiveSubject = new BehaviorSubject<boolean>(true); // True by default
applications$ = this.applicationsSubject.asObservable();
appsSummary$ = this.appsSummarySubject.asObservable();
isReportFromArchive$ = this.isReportFromArchiveSubject.asObservable();
private isLoadingSubject = new BehaviorSubject<boolean>(false);
isLoading$ = this.isLoadingSubject.asObservable();
@@ -34,7 +42,10 @@ export class RiskInsightsDataService {
appAtRiskMembers: AppAtRiskMembersDialogParams | null = null;
atRiskAppDetails: AtRiskApplicationDetail[] | null = null;
constructor(private reportService: RiskInsightsReportService) {}
constructor(
private reportService: RiskInsightsReportService,
private riskInsightsApiService: RiskInsightsApiService,
) {}
/**
* Fetches the applications report and updates the applicationsSubject.
@@ -52,13 +63,14 @@ export class RiskInsightsDataService {
finalize(() => {
this.isLoadingSubject.next(false);
this.isRefreshingSubject.next(false);
this.dataLastUpdatedSubject.next(new Date());
}),
)
.subscribe({
next: (reports: ApplicationHealthReportDetail[]) => {
this.applicationsSubject.next(reports);
this.errorSubject.next(null);
this.appsSummarySubject.next(this.reportService.generateApplicationsSummary(reports));
this.dataLastUpdatedSubject.next(new Date());
},
error: () => {
this.applicationsSubject.next([]);
@@ -66,6 +78,57 @@ export class RiskInsightsDataService {
});
}
fetchApplicationsReportFromCache(organizationId: string) {
return this.riskInsightsApiService
.getRiskInsightsReport(organizationId as OrganizationId)
.pipe(
switchMap(async (reportFromArchive) => {
if (!reportFromArchive || !reportFromArchive?.reportDate) {
this.fetchApplicationsReport(organizationId);
return {
report: [],
summary: null,
fromDb: false,
lastUpdated: new Date(),
};
} else {
const [report, summary] = await this.reportService.decryptRiskInsightsReport(
organizationId as OrganizationId,
reportFromArchive,
);
return {
report,
summary,
fromDb: true,
lastUpdated: new Date(reportFromArchive.reportDate),
};
}
}),
)
.subscribe({
next: ({ report, summary, fromDb, lastUpdated }) => {
if (fromDb) {
this.applicationsSubject.next(report);
this.errorSubject.next(null);
this.appsSummarySubject.next(summary);
}
this.isReportFromArchiveSubject.next(fromDb);
this.dataLastUpdatedSubject.next(lastUpdated);
},
error: (error: unknown) => {
this.errorSubject.next((error as Error).message);
this.applicationsSubject.next([]);
},
});
}
isLoadingData(started: boolean): void {
this.isLoadingSubject.next(started);
this.isRefreshingSubject.next(started);
}
refreshApplicationsReport(organizationId: string): void {
this.fetchApplicationsReport(organizationId, true);
}

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe
// @ts-strict-ignore
import { concatMap, first, firstValueFrom, from, map, Observable, takeWhile, zip } from "rxjs";
import { concatMap, first, from, map, Observable, zip } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
@@ -184,9 +184,12 @@ export class RiskInsightsReportService {
throw new Error("Organization key not found");
}
const reportWithSummary = { details, summary };
const reportContentEncryptionKey = await this.keyGeneratorService.createKey(512);
const reportEncrypted = await this.encryptService.encryptString(
JSON.stringify(details),
JSON.stringify(reportWithSummary),
reportContentEncryptionKey,
);
@@ -200,21 +203,15 @@ export class RiskInsightsReportService {
key: wrappedReportContentEncryptionKey.encryptedString,
};
const criticalApps = await firstValueFrom(
this.criticalAppsService
.getAppsListForOrg(organizationId)
.pipe(takeWhile((apps) => apps !== null && apps.length > 0)),
);
const riskInsightReport = {
organizationId: organizationId,
date: new Date().toISOString(),
reportData: JSON.stringify(reportDataWithWrappedKey),
totalMembers: summary.totalMemberCount,
totalAtRiskMembers: summary.totalAtRiskMemberCount,
totalApplications: summary.totalApplicationCount,
totalAtRiskApplications: summary.totalAtRiskApplicationCount,
totalCriticalApplications: criticalApps.length,
totalMembers: 0,
totalAtRiskMembers: 0,
totalApplications: 0,
totalAtRiskApplications: 0,
totalCriticalApplications: 0,
};
return riskInsightReport;
@@ -244,16 +241,11 @@ export class RiskInsightsReportService {
unwrappedReportContentEncryptionKey,
);
const reportJson: ApplicationHealthReportDetail[] = JSON.parse(reportUnencrypted);
const reportWithSummary = JSON.parse(reportUnencrypted);
const reportJson = reportWithSummary.details;
const reportSummary = reportWithSummary.summary;
const summary: ApplicationHealthReportSummary = {
totalMemberCount: riskInsightsReportResponse.totalMembers,
totalAtRiskMemberCount: riskInsightsReportResponse.totalAtRiskMembers,
totalApplicationCount: riskInsightsReportResponse.totalApplications,
totalAtRiskApplicationCount: riskInsightsReportResponse.totalAtRiskApplications,
};
return [reportJson, summary];
return [reportJson, reportSummary];
} catch {
return [null, null];
}

View File

@@ -42,7 +42,7 @@ import { RiskInsightsComponent } from "./risk-insights.component";
},
{
provide: RiskInsightsDataService,
deps: [RiskInsightsReportService],
deps: [RiskInsightsReportService, RiskInsightsApiService],
},
safeProvider({
provide: CriticalAppsService,

View File

@@ -101,6 +101,10 @@ export class AllApplicationsComponent implements OnInit {
});
async ngOnInit() {
this.isLoading$ = this.dataService.isLoading$;
this.dataService.isLoadingData(true);
this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
@@ -113,105 +117,47 @@ export class AllApplicationsComponent implements OnInit {
.organizations$(userId)
.pipe(getOrganizationById(organizationId));
this.dataService.fetchApplicationsReportFromCache(organizationId as OrganizationId);
combineLatest([
this.riskInsightsApiService.getRiskInsightsReport(organizationId as OrganizationId),
this.criticalAppsService.getAppsListForOrg(organizationId),
organization$,
this.dataService.applications$,
this.dataService.appsSummary$,
this.dataService.isReportFromArchive$,
organization$,
this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId),
])
.pipe(
switchMap(async ([reportArchive, criticalApps, organization, appHealthReport]) => {
if (reportArchive == null) {
const report = await firstValueFrom(this.dataService.applications$);
if (report == null) {
this.dataService.fetchApplicationsReport(organizationId as OrganizationId);
}
return {
report,
criticalApps,
organization,
fromDb: false,
};
} else {
const [report] = await this.reportService.decryptRiskInsightsReport(
organizationId as OrganizationId,
reportArchive,
);
return {
report,
criticalApps,
organization,
fromDb: true,
};
}
}),
map(({ report, criticalApps, organization, fromDb }) => {
const criticalUrls = criticalApps.map((ca) => ca.uri);
map(([report, summary, isReportFromArchive, organization, criticalApps]) => {
const criticalUrls = criticalApps?.map((ca) => ca.uri);
const data = report?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as ApplicationHealthReportDetailWithCriticalFlag[];
return { data, organization, fromDb };
return { report: data, summary, criticalApps, isReportFromArchive, organization };
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(({ data, organization, fromDb }) => {
if (data) {
this.dataSource.data = data;
this.applicationSummary = this.reportService.generateApplicationsSummary(data);
.subscribe(({ report, summary, isReportFromArchive, organization }) => {
if (report) {
this.dataSource.data = report;
this.applicationSummary = summary;
}
if (organization) {
this.organization = organization;
}
if (!fromDb && data && organization) {
if (!isReportFromArchive && report && organization) {
this.atRiskInsightsReport.next({
data,
organization,
data: report,
organization: organization,
summary: this.applicationSummary,
});
}
});
// combineLatest([
// this.dataService.applications$,
// this.criticalAppsService.getAppsListForOrg(organizationId),
// organization$,
// ])
// .pipe(
// takeUntilDestroyed(this.destroyRef),
// skipWhile(([_, __, organization]) => !organization),
// map(([applications, criticalApps, organization]) => {
// const criticalUrls = criticalApps.map((ca) => ca.uri);
// const data = applications?.map((app) => ({
// ...app,
// isMarkedAsCritical: criticalUrls.includes(app.applicationName),
// })) as ApplicationHealthReportDetailWithCriticalFlag[];
// return { data, organization };
// }),
// )
// .subscribe(({ data, organization }) => {
// if (data) {
// this.dataSource.data = data;
// this.applicationSummary = this.reportService.generateApplicationsSummary(data);
// }
// if (organization) {
// this.organization = organization;
// }
// if (data && organization) {
// this.atRiskInsightsReport.next({
// data,
// organization,
// summary: this.applicationSummary,
// });
// }
// });
this.isLoading$ = this.dataService.isLoading$;
this.dataService.isLoadingData(false);
}
}

View File

@@ -2,8 +2,8 @@ import { CommonModule } from "@angular/common";
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { EMPTY, Observable } from "rxjs";
import { map, switchMap } from "rxjs/operators";
import { combineLatest, Observable, of } from "rxjs";
import { map } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@@ -11,7 +11,6 @@ import {
RiskInsightsDataService,
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
import {
ApplicationHealthReportDetail,
DrawerType,
PasswordHealthReportApplicationsResponse,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
@@ -80,9 +79,9 @@ export class RiskInsightsComponent implements OnInit {
private organizationId: string | null = null;
private destroyRef = inject(DestroyRef);
isLoading$: Observable<boolean> = new Observable<boolean>();
isRefreshing$: Observable<boolean> = new Observable<boolean>();
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
isLoading$: Observable<boolean> = this.dataService.isLoading$;
isRefreshing$: Observable<boolean> = this.dataService.isRefreshing$;
dataLastUpdated$: Observable<Date | null> = this.dataService.dataLastUpdated$;
refetching: boolean = false;
constructor(
@@ -106,30 +105,36 @@ export class RiskInsightsComponent implements OnInit {
this.showDebugTabs = devFlagEnabled("showRiskInsightsDebug");
this.isLoading$ = this.dataService.isLoading$;
this.isRefreshing$ = this.dataService.isRefreshing$;
this.dataLastUpdated$ = this.dataService.dataLastUpdated$;
this.route.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map((params) => params.get("organizationId")),
switchMap((orgId: string | null) => {
if (orgId) {
this.organizationId = orgId;
// this.dataService.fetchApplicationsReport(orgId);
this.isLoading$ = this.dataService.isLoading$;
this.isRefreshing$ = this.dataService.isRefreshing$;
this.dataLastUpdated$ = this.dataService.dataLastUpdated$;
return this.dataService.applications$;
} else {
return EMPTY;
}
}),
)
.subscribe({
next: (applications: ApplicationHealthReportDetail[] | null) => {
if (applications) {
this.appsCount = applications.length;
}
this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId);
},
.subscribe((orgId: string | null) => {
if (orgId) {
this.criticalAppsService.setOrganizationId(orgId as OrganizationId);
this.organizationId = orgId;
}
});
combineLatest([
this.dataService.applications$,
this.dataService.dataLastUpdated$,
this.criticalAppsService.getAppsListForOrg(this.organizationId as string),
])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([applications, dataLastUpdated, criticalApps]) => {
this.dataLastUpdated$ = of(dataLastUpdated);
if (applications) {
this.appsCount = applications.length;
this.criticalAppsCount = criticalApps.length;
this.dataLastUpdated = new Date(dataLastUpdated);
}
});
}