1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 19:53:59 +00:00

PM-20577 save report summary to database

This commit is contained in:
voommen-livefront
2025-06-20 12:39:57 -05:00
parent e47bcb2d2a
commit ae4ab8a606
9 changed files with 114 additions and 60 deletions

View File

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

View File

@@ -39,11 +39,11 @@ describe("RiskInsightsApiService", () => {
apiService.send.mockReturnValue(Promise.resolve(response));
service.saveRiskInsightsReport(orgId, request).subscribe((result) => {
service.saveRiskInsightsReport(request).subscribe((result) => {
expect(result).toEqual(response);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`/reports/risk-insights-report/${orgId.toString()}`,
"POST",
`/reports/organization-reports`,
request.data,
true,
true,

View File

@@ -13,12 +13,11 @@ export class RiskInsightsApiService {
constructor(private apiService: ApiService) {}
saveRiskInsightsReport(
orgId: OrganizationId,
request: SaveRiskInsightsReportRequest,
): Observable<SaveRiskInsightsReportResponse> {
const dbResponse = this.apiService.send(
"PUT",
`/reports/risk-insights-report/${orgId.toString()}`,
"POST",
`/reports/organization-reports`,
request.data,
true,
true,
@@ -29,7 +28,7 @@ export class RiskInsightsApiService {
getRiskInsightsReport(orgId: OrganizationId): Observable<GetRiskInsightsReportResponse | null> {
const dbResponse = this.apiService
.send("GET", `/reports/risk-insights-report/${orgId.toString()}`, null, true, true)
.send("GET", `/reports/organization-reports/latest/${orgId.toString()}`, null, true, true)
.catch((error: any): any => {
if (error.statusCode === 404) {
return null; // Handle 404 by returning null or an appropriate default value

View File

@@ -1,7 +1,10 @@
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject } from "rxjs";
import { finalize, switchMap } from "rxjs/operators";
import { switchMap } from "rxjs/operators";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
AppAtRiskMembersDialogParams,
@@ -35,6 +38,9 @@ export class RiskInsightsDataService {
private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();
private cipherViewsForOrganizationSubject = new BehaviorSubject<CipherView[]>([]);
cipherViewsForOrganization$ = this.cipherViewsForOrganizationSubject.asObservable();
openDrawer = false;
drawerInvokerId: string = "";
activeDrawerType: DrawerType = DrawerType.None;
@@ -45,6 +51,7 @@ export class RiskInsightsDataService {
constructor(
private reportService: RiskInsightsReportService,
private riskInsightsApiService: RiskInsightsApiService,
private cipherService: CipherService,
) {}
/**
@@ -52,19 +59,9 @@ export class RiskInsightsDataService {
* @param organizationId The ID of the organization.
*/
fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void {
if (isRefresh) {
this.isRefreshingSubject.next(true);
} else {
this.isLoadingSubject.next(true);
}
this.reportService
.generateApplicationsReport$(organizationId)
.pipe(
finalize(() => {
this.isLoadingSubject.next(false);
this.isRefreshingSubject.next(false);
}),
)
.pipe(takeUntilDestroyed())
.subscribe({
next: (reports: ApplicationHealthReportDetail[]) => {
this.applicationsSubject.next(reports);
@@ -83,13 +80,13 @@ export class RiskInsightsDataService {
.getRiskInsightsReport(organizationId as OrganizationId)
.pipe(
switchMap(async (reportFromArchive) => {
if (!reportFromArchive || !reportFromArchive?.reportDate) {
if (!reportFromArchive || !reportFromArchive?.date) {
this.fetchApplicationsReport(organizationId);
return {
report: [],
summary: null,
fromDb: false,
fromArchive: false,
lastUpdated: new Date(),
};
} else {
@@ -101,20 +98,23 @@ export class RiskInsightsDataService {
return {
report,
summary,
fromDb: true,
lastUpdated: new Date(reportFromArchive.reportDate),
fromArchive: true,
lastUpdated: new Date(reportFromArchive.date),
};
}
}),
)
.subscribe({
next: ({ report, summary, fromDb, lastUpdated }) => {
if (fromDb) {
next: ({ report, summary, fromArchive, lastUpdated }) => {
// in this block, only set the applicationsSubject and appsSummarySubject if the report is from archive
// the fetchApplicationsReport will set them if the report is not from archive
if (fromArchive) {
this.applicationsSubject.next(report);
this.errorSubject.next(null);
this.appsSummarySubject.next(summary);
}
this.isReportFromArchiveSubject.next(fromDb);
this.isReportFromArchiveSubject.next(fromArchive);
this.dataLastUpdatedSubject.next(lastUpdated);
},
error: (error: unknown) => {
@@ -124,11 +124,31 @@ export class RiskInsightsDataService {
});
}
async fetchCipherViewsForOrganization(
organizationId: OrganizationId,
isRefresh: boolean = false,
): Promise<void> {
if (isRefresh) {
this.cipherViewsForOrganizationSubject.next([]);
}
if (this.cipherViewsForOrganizationSubject.value) {
return;
}
const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId);
this.cipherViewsForOrganizationSubject.next(cipherViews);
}
isLoadingData(started: boolean): void {
this.isLoadingSubject.next(started);
this.isRefreshingSubject.next(started);
}
setReportFromArchiveStatus(isFromArchive: boolean): void {
this.isReportFromArchiveSubject.next(isFromArchive);
}
refreshApplicationsReport(organizationId: string): void {
this.fetchApplicationsReport(organizationId, true);
}

View File

@@ -256,10 +256,8 @@ export class RiskInsightsReportService {
async identifyCiphers(
data: ApplicationHealthReportDetail[],
organizationId: string,
cipherViews: CipherView[],
): Promise<ApplicationHealthReportDetailWithCriticalFlagAndCipher[]> {
const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId);
const dataWithCiphers = data.map(
(app, index) =>
({

View File

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

View File

@@ -4,13 +4,13 @@ import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
debounceTime,
firstValueFrom,
map,
Observable,
of,
switchMap,
zip,
} from "rxjs";
import {
@@ -85,7 +85,6 @@ export class AllApplicationsComponent implements OnInit {
destroyRef = inject(DestroyRef);
isLoading$: Observable<boolean> = of(false);
isCriticalAppsFeatureEnabled = false;
private atRiskInsightsReport = new BehaviorSubject<{
data: ApplicationHealthReportDetailWithCriticalFlag[];
@@ -107,10 +106,6 @@ export class AllApplicationsComponent implements OnInit {
this.dataService.isLoadingData(true);
this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
@@ -119,25 +114,70 @@ export class AllApplicationsComponent implements OnInit {
.organizations$(userId)
.pipe(getOrganizationById(organizationId));
await this.dataService.fetchCipherViewsForOrganization(organizationId as OrganizationId);
this.dataService.fetchApplicationsReportFromCache(organizationId as OrganizationId);
zip([
combineLatest([
this.dataService.applications$,
this.dataService.appsSummary$,
this.dataService.isReportFromArchive$,
organization$,
this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId),
this.dataService.cipherViewsForOrganization$,
])
.pipe(
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[];
map(
([
report,
summary,
isReportFromArchive,
organization,
criticalApps,
cipherViewsForOrg,
]) => {
const criticalUrls = criticalApps?.map((ca) => ca.uri);
const data = report?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as ApplicationHealthReportDetailWithCriticalFlag[];
return { report: data, summary, criticalApps, isReportFromArchive, organization };
}),
return {
report: data,
summary,
criticalApps,
isReportFromArchive,
organization,
cipherViewsForOrg,
};
},
),
switchMap(
async ({
report,
summary,
isReportFromArchive,
organization,
criticalApps,
cipherViewsForOrg,
}) => {
if (report && organization) {
const dataWithCiphers = await this.reportService.identifyCiphers(
report,
cipherViewsForOrg,
);
return {
report: dataWithCiphers,
summary,
isReportFromArchive,
organization,
criticalApps,
};
}
return { report: [], summary, isReportFromArchive, organization, criticalApps: [] };
},
),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(({ report, summary, criticalApps, isReportFromArchive, organization }) => {
@@ -150,7 +190,7 @@ export class AllApplicationsComponent implements OnInit {
this.organization = organization;
}
if (!isReportFromArchive && report && organization && summary && criticalApps) {
if (!isReportFromArchive && report && organization && summary) {
this.atRiskInsightsReport.next({
data: report,
organization: organization,
@@ -200,14 +240,14 @@ export class AllApplicationsComponent implements OnInit {
const request = { data: reportData };
try {
const response = await firstValueFrom(
this.riskInsightsApiService.saveRiskInsightsReport(
this.organization.id as OrganizationId,
request,
),
this.riskInsightsApiService.saveRiskInsightsReport(request),
);
return response;
} catch {
/* continue as usual */
} finally {
// now that we have saved the data, we are in sync with the archive
this.dataService.setReportFromArchiveStatus(true);
}
return null;

View File

@@ -76,23 +76,22 @@ export class CriticalApplicationsComponent implements OnInit {
combineLatest([
this.dataService.applications$,
this.criticalAppsService.getAppsListForOrg(this.organizationId),
this.dataService.cipherViewsForOrganization$,
])
.pipe(
takeUntilDestroyed(this.destroyRef),
map(([applications, criticalApps]) => {
map(([applications, criticalApps, cipherViewsForOrg]) => {
const criticalUrls = criticalApps.map((ca) => ca.uri);
const data = applications?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as ApplicationHealthReportDetailWithCriticalFlag[];
return data?.filter((app) => app.isMarkedAsCritical);
const dataWithCriticalAppsFlag = data?.filter((app) => app.isMarkedAsCritical);
return { data: dataWithCriticalAppsFlag, cipherViews: cipherViewsForOrg };
}),
switchMap(async (data) => {
switchMap(async ({ data, cipherViews }) => {
if (data) {
const dataWithCiphers = await this.reportService.identifyCiphers(
data,
this.organizationId,
);
const dataWithCiphers = await this.reportService.identifyCiphers(data, cipherViews);
return dataWithCiphers;
}
return null;

View File

@@ -14,7 +14,6 @@ import {
DrawerType,
PasswordHealthReportApplicationsResponse,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
@@ -84,7 +83,6 @@ export class RiskInsightsComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router,
private configService: ConfigService,
protected dataService: RiskInsightsDataService,
private criticalAppsService: CriticalAppsService,
) {