1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

demo prep for risk insights

This commit is contained in:
Tom
2024-12-12 13:11:13 -05:00
parent e23f579760
commit 86095b672c
9 changed files with 193 additions and 66 deletions

View File

@@ -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": {

View File

@@ -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<string, Observable<ApplicationHealthReportDetail[]>>();
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<ApplicationHealthReportDetail[]> {
// 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);
}
}
}

View File

@@ -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
],

View File

@@ -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 {}

View File

@@ -1,12 +1,7 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
<tools-risk-insights-loading></tools-risk-insights-loading>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<div class="tw-mt-4" *ngIf="!loading && !dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
@@ -34,15 +29,15 @@
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
>
</tools-card>
</div>
@@ -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 @@
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th *ngIf="isCritialAppsFeatureEnabled"></th>
<th *ngIf="isCritialAppsFeatureEnabled$ | async"></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
@@ -79,34 +74,34 @@
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td *ngIf="isCritialAppsFeatureEnabled">
<td *ngIf="isCritialAppsFeatureEnabled$ | async">
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
[checked]="selectedIds.has(r.applicationName)"
(change)="onCheckboxChange(r.applicationName, $event)"
/>
</td>
<td bitCell>
<span>{{ r.name }}</span>
<span>{{ r.applicationName }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
{{ r.atRiskPasswordCount }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
{{ r.passwordCount }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
{{ r.atRiskMemberDetails.length }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
{{ r.memberCount }}
</td>
</tr>
</ng-template>

View File

@@ -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<any>();
protected dataSource = new TableDataSource<ApplicationHealthReportDetail>();
protected selectedIds: Set<number> = new Set<number>();
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<boolean>;
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;

View File

@@ -0,0 +1,8 @@
<div class="tw-flex-col tw-flex tw-justify-center tw-items-center tw-gap-5 tw-mt-4">
<i
class="bwi bwi-lg bwi-spinner bwi-spin text-primary"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<h2 bitTypography="h1">{{ "generatingRiskInsights" | i18n }}</h2>
</div>

View File

@@ -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() {}
}

View File

@@ -2,7 +2,7 @@
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
<div class="tw-text-muted tw-max-w-4xl tw-mb-2">
{{ "reviewAtRiskPasswords" | i18n }}
&nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
<!-- &nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a> -->
</div>
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
<i class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted" aria-hidden="true"></i>
@@ -29,7 +29,7 @@
</ng-template>
<tools-critical-applications></tools-critical-applications>
</bit-tab>
<bit-tab label="Raw Data">
<!-- <bit-tab label="Raw Data">
<tools-password-health></tools-password-health>
</bit-tab>
<bit-tab label="Raw Data + members">
@@ -37,7 +37,7 @@
</bit-tab>
<bit-tab label="Raw Data + uri">
<tools-password-health-members-uri></tools-password-health-members-uri>
</bit-tab>
</bit-tab> -->
<!-- <bit-tab>
<ng-template bitTabLabel>
<i class="bwi bwi-envelope"></i>