mirror of
https://github.com/bitwarden/browser
synced 2026-02-16 08:34:39 +00:00
add org-wide at-risk member dialog
This commit is contained in:
@@ -90,3 +90,12 @@ export type MemberDetailsFlat = {
|
||||
email: string;
|
||||
cipherId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* At risk member detail that contains the email
|
||||
* and the count of at risk ciphers
|
||||
*/
|
||||
export type AtRiskMemberDetail = {
|
||||
email: string;
|
||||
atRiskPasswordCount: number;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
ApplicationHealthReportSummary,
|
||||
AtRiskMemberDetail,
|
||||
CipherHealthReportDetail,
|
||||
CipherHealthReportUriDetail,
|
||||
ExposedPasswordDetail,
|
||||
@@ -92,6 +93,30 @@ export class RiskInsightsReportService {
|
||||
return results$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
|
||||
*/
|
||||
generateAtRiskMemberList(
|
||||
cipherHealthReportDetails: ApplicationHealthReportDetail[],
|
||||
): AtRiskMemberDetail[] {
|
||||
const memberRiskMap = new Map<string, number>();
|
||||
|
||||
cipherHealthReportDetails.forEach((app) => {
|
||||
app.atRiskMemberDetails.forEach((member) => {
|
||||
if (memberRiskMap.has(member.email)) {
|
||||
memberRiskMap.set(member.email, memberRiskMap.get(member.email) + 1);
|
||||
} else {
|
||||
memberRiskMap.set(member.email, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
|
||||
email,
|
||||
atRiskPasswordCount,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the summary from the application health report. Returns total members and applications as well
|
||||
* as the total at risk members and at risk applications
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<bit-dialog>
|
||||
<ng-container bitDialogTitle>
|
||||
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} </span>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<span bitTypography="body2" class="tw-text-muted">{{
|
||||
"atRiskMembersDescription" | i18n
|
||||
}}</span>
|
||||
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
|
||||
<div bitTypography="body2" class="tw-font-bold">{{ "email" | i18n }}</div>
|
||||
<div bitTypography="body2" class="tw-font-bold">{{ "atRiskPasswords" | i18n }}</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let member of atRiskMembers">
|
||||
<div class="tw-flex tw-justify-between tw-mt-2">
|
||||
<div>{{ member.email }}</div>
|
||||
<div>{{ member.atRiskPasswordCount }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||
{{ "ok" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
export const openOrgAtRiskMembersDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: AtRiskMemberDetail[],
|
||||
) =>
|
||||
dialogService.open<boolean, AtRiskMemberDetail[]>(OrgAtRiskMembersDialogComponent, {
|
||||
data: dialogConfig,
|
||||
});
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./org-at-risk-members-dialog.component.html",
|
||||
imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule],
|
||||
})
|
||||
export class OrgAtRiskMembersDialogComponent {
|
||||
constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {}
|
||||
}
|
||||
@@ -5,6 +5,11 @@
|
||||
{{ "reviewAtRiskPasswords" | i18n }}
|
||||
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||
</div>
|
||||
<div>
|
||||
<button bitButton buttonType="primary" type="button" (click)="showAtRiskMembers()">
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
|
||||
>
|
||||
|
||||
@@ -11,15 +11,19 @@ import {
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import {
|
||||
ApplicationHealthReportDetail,
|
||||
AtRiskMemberDetail,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
|
||||
import { AsyncActionsModule, ButtonModule, DialogService, TabsModule } from "@bitwarden/components";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
|
||||
import { AllApplicationsComponent } from "./all-applications.component";
|
||||
import { CriticalApplicationsComponent } from "./critical-applications.component";
|
||||
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
|
||||
import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component";
|
||||
|
||||
export enum RiskInsightsTabType {
|
||||
AllApps = 0,
|
||||
@@ -60,12 +64,15 @@ export class RiskInsightsComponent implements OnInit {
|
||||
isRefreshing$: Observable<boolean> = new Observable<boolean>();
|
||||
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
|
||||
refetching: boolean = false;
|
||||
private atRiskMembers: AtRiskMemberDetail[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private dataService: RiskInsightsDataService,
|
||||
private dialogService: DialogService,
|
||||
private riskInsightsReportService: RiskInsightsReportService,
|
||||
) {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||
@@ -98,6 +105,8 @@ export class RiskInsightsComponent implements OnInit {
|
||||
next: (applications: ApplicationHealthReportDetail[] | null) => {
|
||||
if (applications) {
|
||||
this.appsCount = applications.length;
|
||||
this.atRiskMembers =
|
||||
this.riskInsightsReportService.generateAtRiskMemberList(applications);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -113,6 +122,10 @@ export class RiskInsightsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
showAtRiskMembers = async () => {
|
||||
this.dialogService.open(OrgAtRiskMembersDialogComponent, { data: this.atRiskMembers });
|
||||
};
|
||||
|
||||
async onTabChange(newIndex: number): Promise<void> {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
|
||||
Reference in New Issue
Block a user