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