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:
@@ -183,7 +183,7 @@ export interface SaveRiskInsightsReportResponse {
|
||||
export interface GetRiskInsightsReportResponse {
|
||||
id: string;
|
||||
organizationId: OrganizationId;
|
||||
date: string;
|
||||
reportDate: string;
|
||||
reportData: string;
|
||||
totalMembers: number;
|
||||
totalAtRiskMembers: number;
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
},
|
||||
{
|
||||
provide: RiskInsightsDataService,
|
||||
deps: [RiskInsightsReportService],
|
||||
deps: [RiskInsightsReportService, RiskInsightsApiService],
|
||||
},
|
||||
safeProvider({
|
||||
provide: CriticalAppsService,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user