mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 11:43:46 +00:00
Moved reports files into tools ownership and updated the imports (#7222)
This commit is contained in:
14
apps/web/src/app/tools/reports/icons/report-breach.icon.ts
Normal file
14
apps/web/src/app/tools/reports/icons/report-breach.icon.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportBreach = svgIcon`
|
||||
<svg width="58" height="75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39.569 74H13.007a7 7 0 0 1-7-7V31.077a7 7 0 0 1 7-7h19.101a7 7 0 0 1 4.988 2.088l7.46 7.576a7 7 0 0 1 2.013 4.912V67a7 7 0 0 1-7 7Z" fill="#175DDC" stroke="#fff" stroke-width="2" />
|
||||
<path d="M44.576 69.055H18.015a7 7 0 0 1-7-7V26.132a7 7 0 0 1 7-7h19.1a7 7 0 0 1 4.988 2.088l7.46 7.576a7 7 0 0 1 2.013 4.911v28.348a7 7 0 0 1-7 7Z" fill="#175DDC" stroke="#fff" stroke-width="2" />
|
||||
<path d="M50 63.698H23.439a7 7 0 0 1-7-7V20.775a7 7 0 0 1 7-7h19.1a7 7 0 0 1 4.988 2.088l7.46 7.575A7 7 0 0 1 57 28.35v28.348a7 7 0 0 1-7 7Z" fill="#175DDC" stroke="#fff" stroke-width="2" />
|
||||
<path d="M44.648 13.599v3.95a8 8 0 0 0 8 8h4.518" stroke="#fff" stroke-width="2" />
|
||||
<path stroke="#fff" stroke-width="2" stroke-linecap="round" d="M23.533 37.736H49.49M23.533 46.802H49.49M23.533 42.269H49.49M23.533 55.456H49.49M23.533 50.923H49.49" />
|
||||
<path d="M1 16.483C1 7.944 8.013 1 16.69 1c8.678 0 15.691 6.944 15.691 15.483 0 8.54-7.013 15.484-15.69 15.484C8.012 31.967 1 25.023 1 16.484Z" fill="#518FFF" stroke="#fff" stroke-width="2" />
|
||||
<path d="m16.562 7.979.1 11.538" stroke="#fff" stroke-width="2" stroke-linecap="round" />
|
||||
<ellipse rx="1.252" ry="1.236" transform="rotate(-.479 2802.219 -1964.476) skewX(.012)" fill="#fff" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportExposedPasswords = svgIcon`
|
||||
<svg width="101" height="77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.374 50.192a26.42 26.42 0 0 0 9.111 1.608c14.34 0 25.965-11.372 25.965-25.4 0-.337-.007-.673-.02-1.008h25.299v34.85H32.374v-10.05Z" fill="currentColor" />
|
||||
<path d="M15.805 26.4c0 14.028 11.625 25.4 25.965 25.4s25.964-11.372 25.964-25.4C67.734 12.372 56.11 1 41.77 1 27.43 1 15.805 12.372 15.805 26.4Z" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M27.914 47.849a1 1 0 0 0-2 0h2Zm68.288-26.792a2.12 2.12 0 0 1 2.14 2.11h2c0-2.253-1.83-4.11-4.14-4.11v2Zm2.14 2.11v40.552h2V23.167h-2Zm0 40.552c0 1.172-.958 2.11-2.14 2.11v2c2.25 0 4.14-1.798 4.14-4.11h-2Zm-2.14 2.11H30.054v2h66.148v-2Zm-66.148 0a2.12 2.12 0 0 1-2.14-2.11h-2a4.12 4.12 0 0 0 4.14 4.11v-2Zm-2.14-2.11V47.85h-2v15.87h2Zm39.254-42.662h29.034v-2H67.168v2Z" fill="#fff" />
|
||||
<path d="M67.203 25.56h25.64v34.85H32.487V50.011" stroke="#fff" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M47.343 76h31.571" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M57.557 66.83V76M67.771 66.83V76" stroke="#fff" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="m20.995 42.873-3.972 3.972-14.61 14.61a3.413 3.413 0 0 0 0 4.826v0a3.413 3.413 0 0 0 4.827 0l14.61-14.61 3.972-3.972" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M86.037 32.488H71.845M86.037 37.81H76.28M71.845 37.81h-6.652M86.037 43.132h-6.209M74.95 43.132H61.2M86.037 48.454H71.845M66.967 48.454h-7.54M86.037 53.776H66.08M61.201 53.776h-11.53M44.793 53.776h-7.096" stroke="#fff" stroke-width="2" stroke-linecap="round" />
|
||||
<rect width="40.801" height="9.757" rx="4" transform="matrix(-1 0 0 1 61.201 14.748)" stroke="#fff" stroke-width="2" />
|
||||
<path d="M16.852 33.375h28.375a4 4 0 0 1 4 4v1.757a4 4 0 0 1-4 4H22.174M66.523 33.375h-3.539a4 4 0 0 0-4 4v3.761c0 1.102.894 1.996 1.996 1.996v0" stroke="#fff" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportInactiveTwoFactor = svgIcon`
|
||||
<svg width="42" height="75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" stroke="#fff" stroke-width="2" d="M1 13.121h39.595v48.758H1z" />
|
||||
<rect x="1" y="1" width="39.595" height="73" rx="8" stroke="#fff" stroke-width="2" />
|
||||
<path stroke="#fff" stroke-width="2" stroke-linecap="round" d="M12.344 8.091h16.907M18.907 67.424h3.025M31.503 32.515c-2.047-4.337-6.717-7.061-11.73-6.414a11.356 11.356 0 0 0-9.125 7.126M10.816 42.016c2.047 4.337 6.718 7.062 11.73 6.414 4.346-.562 7.8-3.51 9.213-7.358" />
|
||||
<path d="m33.584 29.293-1.295 4.625-4.625-1.295M8.523 44.725l1.441-4.581 4.582 1.441" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportReusedPasswords = svgIcon`
|
||||
<svg width="102" height="102" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.983 15.06a35.664 35.664 0 0 1 14.531 6.27c16.164 11.78 19.585 34.613 7.643 51a37.227 37.227 0 0 1-6.81 7.138m-32.842 6.697a35.708 35.708 0 0 1-11.239-5.495c-16.163-11.78-19.585-34.613-7.642-51a37.55 37.55 0 0 1 3.295-3.929" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M93.909 64.598H7.72c-.708 0-1.275-.662-1.275-1.49V40.273c0-.828.567-1.49 1.275-1.49H93.91c.708 0 1.275.663 1.275 1.49v22.837c.047.827-.567 1.49-1.275 1.49Z" fill="currentColor" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M21.532 52.186v-5.965M21.532 52.187l5.748-1.844M21.532 52.186l3.524 4.881M21.531 52.186l-3.47 4.881M21.532 52.187l-5.694-1.844M40.944 52.186v-5.965M40.944 52.187l5.694-1.844M40.944 52.187l3.525 4.88M40.944 52.187l-3.525 4.88M40.944 52.187l-5.694-1.844M54.849 57.337h11.294M74.21 57.337h11.295M41.75 83l.71 4.75-4.75.71M58.664 18.66 56 14.665 59.996 12" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportUnsecuredWebsites = svgIcon`
|
||||
<svg width="113" height="76" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.71 12.983h110.362v55.11a6 6 0 0 1-6 6H7.711a6 6 0 0 1-6-6v-55.11Z" fill="currentColor" />
|
||||
<rect x="1" y="1.073" width="110.5" height="73.454" rx="9" stroke="#fff" stroke-width="2" />
|
||||
<path d="M89.48 8.048V7.47M96.363 8.048V7.47M103.246 8.048V7.47" stroke="#fff" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M0 12.983h111.217" stroke="#fff" stroke-width="2" />
|
||||
<path d="m93.236 44.384-18.42-11.026 2.93 21.266 5.582-5.237 4.27 6.46 2.98-1.971-4.26-6.446 6.918-3.046Z" fill="#175DDC" stroke="#fff" stroke-width="2" stroke-linejoin="round" />
|
||||
<rect width="96.673" height="6.886" rx="3.443" transform="matrix(-1 0 0 1 104.373 18.721)" stroke="#fff" />
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const ReportWeakPasswords = svgIcon`
|
||||
<svg width="78" height="78" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M66.493 64.415V77H9.979V64.324M9.979 44.065V32.106h56.514v12.148" stroke="#fff" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M75.44 64.852H2.085c-.603 0-1.085-.555-1.085-1.25V44.448c0-.694.482-1.25 1.085-1.25H75.44c.603 0 1.085.556 1.085 1.25v19.156c.04.694-.482 1.25-1.085 1.25Z" fill="currentColor" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M13.84 54.56v-5.077M13.84 54.56l4.893-1.57M13.84 54.56l3 4.153M13.84 54.56l-2.954 4.153M13.84 54.56l-4.846-1.57M30.363 54.56v-5.077M30.363 54.56l4.846-1.57M30.363 54.56l3 4.153M30.363 54.56l-3 4.153M30.363 54.56l-4.846-1.57M42.197 59.042h9.506M58.57 59.042h9.507" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M20.863 31.364c-.274-5.285 0-15.817 1.093-18.863 1.276-3.554 6.233-10.826 15.856-11.482 4.83-.273 15.2 2.296 18.043 14.763" stroke="#fff" stroke-width="2" />
|
||||
</svg>
|
||||
`;
|
||||
3
apps/web/src/app/tools/reports/index.ts
Normal file
3
apps/web/src/app/tools/reports/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./reports.module";
|
||||
export * from "./shared";
|
||||
export * from "./reports";
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "dataBreachReport" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{ "breachDesc" | i18n }}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="username">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
name="Username"
|
||||
class="form-control"
|
||||
[(ngModel)]="username"
|
||||
required
|
||||
/>
|
||||
<small class="form-text text-muted">{{ "breachCheckUsernameEmail" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
{{ "checkBreaches" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-4" *ngIf="!form.loading && checkedUsername">
|
||||
<p *ngIf="error">{{ "reportError" | i18n }}...</p>
|
||||
<ng-container *ngIf="!error">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!breachedAccounts.length">
|
||||
{{ "breachUsernameNotFound" | i18n: checkedUsername }}
|
||||
</app-callout>
|
||||
<app-callout type="danger" title="{{ 'breachFound' | i18n }}" *ngIf="breachedAccounts.length">
|
||||
{{ "breachUsernameFound" | i18n: checkedUsername : breachedAccounts.length }}
|
||||
</app-callout>
|
||||
<ul class="list-group list-group-breach" *ngIf="breachedAccounts.length">
|
||||
<li *ngFor="let a of breachedAccounts" class="list-group-item min-height-fix">
|
||||
<div class="row">
|
||||
<div class="col-2 text-center">
|
||||
<img [src]="a.logoPath" alt="" class="img-fluid" />
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<h3 class="text-lg">{{ a.title }}</h3>
|
||||
<p [innerHTML]="a.description"></p>
|
||||
<p class="mb-1">{{ "compromisedData" | i18n }}:</p>
|
||||
<ul>
|
||||
<li *ngFor="let d of a.dataClasses">{{ d }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<dl>
|
||||
<dt>{{ "website" | i18n }}</dt>
|
||||
<dd>{{ a.domain }}</dd>
|
||||
<dt>{{ "affectedUsers" | i18n }}</dt>
|
||||
<dd>{{ a.pwnCount | number }}</dd>
|
||||
<dt>{{ "breachOccurred" | i18n }}</dt>
|
||||
<dd>{{ a.breachDate | date: "mediumDate" }}</dd>
|
||||
<dt>{{ "breachReported" | i18n }}</dt>
|
||||
<dd>{{ a.addedDate | date: "mediumDate" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { BreachAccountResponse } from "@bitwarden/common/models/response/breach-account.response";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-breach-report",
|
||||
templateUrl: "breach-report.component.html",
|
||||
})
|
||||
export class BreachReportComponent implements OnInit {
|
||||
error = false;
|
||||
username: string;
|
||||
checkedUsername: string;
|
||||
breachedAccounts: BreachAccountResponse[] = [];
|
||||
formPromise: Promise<BreachAccountResponse[]>;
|
||||
|
||||
constructor(
|
||||
private auditService: AuditService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.username = await this.stateService.getEmail();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.error = false;
|
||||
this.username = this.username.toLowerCase();
|
||||
try {
|
||||
this.formPromise = this.auditService.breachedAccounts(this.username);
|
||||
this.breachedAccounts = await this.formPromise;
|
||||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
this.checkedUsername = this.username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AddEditComponent } from "../../../vault/individual-vault/add-edit.component";
|
||||
import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component";
|
||||
|
||||
@Directive()
|
||||
export class CipherReportComponent {
|
||||
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
|
||||
cipherAddEditModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
hasLoaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
organization: Organization;
|
||||
organizations$: Observable<Organization[]>;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
protected organizationService: OrganizationService,
|
||||
) {
|
||||
this.organizations$ = this.organizationService.organizations$;
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
await this.setCiphers();
|
||||
this.loading = false;
|
||||
this.hasLoaded = true;
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
if (!(await this.repromptCipher(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = this.organization != null ? OrgAddEditComponent : AddEditComponent;
|
||||
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
type,
|
||||
this.cipherAddEditModalRef,
|
||||
(comp: OrgAddEditComponent | AddEditComponent) => {
|
||||
if (this.organization != null) {
|
||||
(comp as OrgAddEditComponent).organization = this.organization;
|
||||
comp.organizationId = this.organization.id;
|
||||
}
|
||||
|
||||
comp.cipherId = cipher == null ? null : cipher.id;
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
comp.onSavedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.load();
|
||||
});
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
comp.onDeletedCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.load();
|
||||
});
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
comp.onRestoredCipher.subscribe(async () => {
|
||||
modal.close();
|
||||
await this.load();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
protected async setCiphers() {
|
||||
this.ciphers = [];
|
||||
}
|
||||
|
||||
protected async repromptCipher(c: CipherView) {
|
||||
return (
|
||||
c.reprompt === CipherRepromptType.None ||
|
||||
(await this.passwordRepromptService.showPasswordPrompt())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "exposedPasswordsReport" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{ "exposedPasswordsReportDesc" | i18n }}</p>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="loading" (click)="load()">
|
||||
{{ "checkExposedPasswords" | i18n }}
|
||||
</button>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
|
||||
{{ "noExposedPasswords" | i18n }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true">
|
||||
{{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ c.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="c.organizationId"
|
||||
[organizationName]="c.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "exposedXTimes" | i18n: (exposedPasswordMap.get(c.id) | number) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
@@ -0,0 +1,79 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component";
|
||||
import { cipherData } from "./reports-ciphers.mock";
|
||||
|
||||
describe("ExposedPasswordsReportComponent", () => {
|
||||
let component: ExposedPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<ExposedPasswordsReportComponent>;
|
||||
let auditService: MockProxy<AuditService>;
|
||||
|
||||
beforeEach(() => {
|
||||
auditService = mock<AuditService>();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ExposedPasswordsReportComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: AuditService,
|
||||
useValue: auditService,
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: mock<ModalService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordRepromptService,
|
||||
useValue: mock<PasswordRepromptService>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExposedPasswordsReportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get only ciphers with exposed passwords that the user has "Can Edit" access to', async () => {
|
||||
const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2";
|
||||
const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3";
|
||||
|
||||
jest.spyOn(auditService, "passwordLeaked").mockReturnValue(Promise.resolve<any>(1234));
|
||||
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(cipherData));
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.ciphers.length).toEqual(2);
|
||||
expect(component.ciphers[0].id).toEqual(expectedIdOne);
|
||||
expect(component.ciphers[0].edit).toEqual(true);
|
||||
expect(component.ciphers[1].id).toEqual(expectedIdTwo);
|
||||
expect(component.ciphers[1].edit).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-exposed-passwords-report",
|
||||
templateUrl: "exposed-passwords-report.component.html",
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
disabled = true;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected auditService: AuditService,
|
||||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const exposedPasswordCiphers: CipherView[] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
allCiphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
|
||||
if (exposedCount > 0) {
|
||||
exposedPasswordCiphers.push(ciph);
|
||||
this.exposedPasswordMap.set(id, exposedCount);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
this.ciphers = [...exposedPasswordCiphers];
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
// this will only ever be false from the org view;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ "inactive2faReport" | i18n }}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{ "inactive2faReportDesc" | i18n }}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
|
||||
{{ "noInactive2fa" | i18n }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}">
|
||||
{{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="c.organizationId"
|
||||
[organizationName]="c.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a
|
||||
bitBadge
|
||||
href="{{ cipherDocs.get(c.id) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
*ngIf="cipherDocs.has(c.id)"
|
||||
>
|
||||
{{ "instructions" | i18n }}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
@@ -0,0 +1,84 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { InactiveTwoFactorReportComponent } from "./inactive-two-factor-report.component";
|
||||
import { cipherData } from "./reports-ciphers.mock";
|
||||
|
||||
describe("InactiveTwoFactorReportComponent", () => {
|
||||
let component: InactiveTwoFactorReportComponent;
|
||||
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [InactiveTwoFactorReportComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: mock<ModalService>(),
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordRepromptService,
|
||||
useValue: mock<PasswordRepromptService>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InactiveTwoFactorReportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get only ciphers with domains in the 2fa directory that they have "Can Edit" access to', async () => {
|
||||
const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228xy4";
|
||||
const expectedIdTwo: any = "cbea34a8-bde4-46ad-9d19-b05001227nm5";
|
||||
component.services.set(
|
||||
"101domain.com",
|
||||
"https://help.101domain.com/account-management/account-security/enabling-disabling-two-factor-verification",
|
||||
);
|
||||
component.services.set(
|
||||
"123formbuilder.com",
|
||||
"https://www.123formbuilder.com/docs/multi-factor-authentication-login",
|
||||
);
|
||||
|
||||
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(cipherData));
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.ciphers.length).toEqual(2);
|
||||
expect(component.ciphers[0].id).toEqual(expectedIdOne);
|
||||
expect(component.ciphers[0].edit).toEqual(true);
|
||||
expect(component.ciphers[1].id).toEqual(expectedIdTwo);
|
||||
expect(component.ciphers[1].edit).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-inactive-two-factor-report",
|
||||
templateUrl: "inactive-two-factor-report.component.html",
|
||||
})
|
||||
export class InactiveTwoFactorReportComponent extends CipherReportComponent implements OnInit {
|
||||
services = new Map<string, string>();
|
||||
cipherDocs = new Map<string, string>();
|
||||
disabled = true;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
private logService: LogService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
try {
|
||||
await this.load2fa();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.services.size > 0) {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const inactive2faCiphers: CipherView[] = [];
|
||||
const docs = new Map<string, string>();
|
||||
|
||||
allCiphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
(login.totp != null && login.totp !== "") ||
|
||||
!login.hasUris ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < login.uris.length; i++) {
|
||||
const u = login.uris[i];
|
||||
if (u.uri != null && u.uri !== "") {
|
||||
const uri = u.uri.replace("www.", "");
|
||||
const domain = Utils.getDomain(uri);
|
||||
if (domain != null && this.services.has(domain)) {
|
||||
if (this.services.get(domain) != null) {
|
||||
docs.set(id, this.services.get(domain));
|
||||
}
|
||||
inactive2faCiphers.push(ciph);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.ciphers = [...inactive2faCiphers];
|
||||
this.cipherDocs = docs;
|
||||
}
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
private async load2fa() {
|
||||
if (this.services.size > 0) {
|
||||
return;
|
||||
}
|
||||
const response = await fetch(new Request("https://api.2fa.directory/v3/totp.json"));
|
||||
if (response.status !== 200) {
|
||||
throw new Error();
|
||||
}
|
||||
const responseJson = await response.json();
|
||||
for (const service of responseJson) {
|
||||
const serviceData = service[1];
|
||||
if (serviceData.domain == null) {
|
||||
continue;
|
||||
}
|
||||
if (serviceData.documentation == null) {
|
||||
continue;
|
||||
}
|
||||
if (serviceData["additional-domains"] != null) {
|
||||
for (const additionalDomain of serviceData["additional-domains"]) {
|
||||
this.services.set(additionalDomain, serviceData.documentation);
|
||||
}
|
||||
}
|
||||
this.services.set(serviceData.domain, serviceData.documentation);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts
Normal file
128
apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export const cipherData: any[] = [
|
||||
{
|
||||
initializerKey: 1,
|
||||
id: "cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "Cannot Be Edited",
|
||||
notes: null,
|
||||
isDeleted: false,
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: false,
|
||||
login: {
|
||||
password: "123",
|
||||
},
|
||||
edit: false,
|
||||
viewPassword: true,
|
||||
collectionIds: [],
|
||||
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||
creationDate: "2023-08-03T17:40:59.793Z",
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: null,
|
||||
},
|
||||
{
|
||||
initializerKey: 1,
|
||||
id: "cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "Can Be Edited id ending 2",
|
||||
notes: null,
|
||||
isDeleted: false,
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: false,
|
||||
login: {
|
||||
password: "123",
|
||||
hasUris: true,
|
||||
uris: [
|
||||
{
|
||||
uri: "http://nothing.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
collectionIds: [],
|
||||
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||
creationDate: "2023-08-03T17:40:59.793Z",
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: null,
|
||||
},
|
||||
{
|
||||
initializerKey: 1,
|
||||
id: "cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "Can Be Edited id ending 3",
|
||||
notes: null,
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: false,
|
||||
login: {
|
||||
password: "123",
|
||||
hasUris: true,
|
||||
uris: [
|
||||
{
|
||||
uri: "http://example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
collectionIds: [],
|
||||
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||
creationDate: "2023-08-03T17:40:59.793Z",
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: null,
|
||||
},
|
||||
{
|
||||
initializerKey: 1,
|
||||
id: "cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "Can Be Edited id ending 4",
|
||||
notes: null,
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: false,
|
||||
login: {
|
||||
hasUris: true,
|
||||
uris: [{ uri: "101domain.com" }],
|
||||
},
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
collectionIds: [],
|
||||
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||
creationDate: "2023-08-03T17:40:59.793Z",
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: null,
|
||||
},
|
||||
{
|
||||
initializerKey: 1,
|
||||
id: "cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: "Can Be Edited id ending 5",
|
||||
notes: null,
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: false,
|
||||
login: {
|
||||
hasUris: true,
|
||||
uris: [{ uri: "123formbuilder.com" }],
|
||||
},
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
collectionIds: [],
|
||||
revisionDate: "2023-08-03T17:40:59.793Z",
|
||||
creationDate: "2023-08-03T17:40:59.793Z",
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: null,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "reports" | i18n }}</h1>
|
||||
</div>
|
||||
|
||||
<p>{{ "reportsDesc" | i18n }}</p>
|
||||
|
||||
<app-report-list [reports]="reports"></app-report-list>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { reports, ReportType } from "../reports";
|
||||
import { ReportEntry, ReportVariant } from "../shared";
|
||||
|
||||
@Component({
|
||||
selector: "app-reports-home",
|
||||
templateUrl: "reports-home.component.html",
|
||||
})
|
||||
export class ReportsHomeComponent implements OnInit {
|
||||
reports: ReportEntry[];
|
||||
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const userHasPremium = await this.stateService.getCanAccessPremium();
|
||||
|
||||
const reportRequiresPremium = userHasPremium
|
||||
? ReportVariant.Enabled
|
||||
: ReportVariant.RequiresPremium;
|
||||
|
||||
this.reports = [
|
||||
{
|
||||
...reports[ReportType.ExposedPasswords],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.ReusedPasswords],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.WeakPasswords],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.UnsecuredWebsites],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.Inactive2fa],
|
||||
variant: reportRequiresPremium,
|
||||
},
|
||||
{
|
||||
...reports[ReportType.DataBreach],
|
||||
variant: ReportVariant.Enabled,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ "reusedPasswordsReport" | i18n }}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{ "reusedPasswordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
|
||||
{{ "noReusedPasswords" | i18n }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}">
|
||||
{{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ c.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="c.organizationId"
|
||||
[organizationName]="c.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span bitBadge variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(c.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
@@ -0,0 +1,70 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { cipherData } from "./reports-ciphers.mock";
|
||||
import { ReusedPasswordsReportComponent } from "./reused-passwords-report.component";
|
||||
|
||||
describe("ReusedPasswordsReportComponent", () => {
|
||||
let component: ReusedPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ReusedPasswordsReportComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: mock<ModalService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordRepromptService,
|
||||
useValue: mock<PasswordRepromptService>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ReusedPasswordsReportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get ciphers with reused passwords that the user has "Can Edit" access to', async () => {
|
||||
const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2";
|
||||
const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3";
|
||||
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(cipherData));
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.ciphers.length).toEqual(2);
|
||||
expect(component.ciphers[0].id).toEqual(expectedIdOne);
|
||||
expect(component.ciphers[0].edit).toEqual(true);
|
||||
expect(component.ciphers[1].id).toEqual(expectedIdTwo);
|
||||
expect(component.ciphers[1].edit).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-reused-passwords-report",
|
||||
templateUrl: "reused-passwords-report.component.html",
|
||||
})
|
||||
export class ReusedPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
passwordUseMap: Map<string, number>;
|
||||
disabled = true;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const ciphersWithPasswords: CipherView[] = [];
|
||||
this.passwordUseMap = new Map<string, number>();
|
||||
allCiphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ciphersWithPasswords.push(ciph);
|
||||
if (this.passwordUseMap.has(login.password)) {
|
||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
|
||||
} else {
|
||||
this.passwordUseMap.set(login.password, 1);
|
||||
}
|
||||
});
|
||||
const reusedPasswordCiphers = ciphersWithPasswords.filter(
|
||||
(c) =>
|
||||
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
|
||||
);
|
||||
this.ciphers = reusedPasswordCiphers;
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
// this will only ever be false from an organization view
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ "unsecuredWebsitesReport" | i18n }}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{ "unsecuredWebsitesReportDesc" | i18n }}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
|
||||
{{ "noUnsecuredWebsites" | i18n }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}">
|
||||
{{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="c.organizationId"
|
||||
[organizationName]="c.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
@@ -0,0 +1,70 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { cipherData } from "./reports-ciphers.mock";
|
||||
import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.component";
|
||||
|
||||
describe("UnsecuredWebsitesReportComponent", () => {
|
||||
let component: UnsecuredWebsitesReportComponent;
|
||||
let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UnsecuredWebsitesReportComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: mock<ModalService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordRepromptService,
|
||||
useValue: mock<PasswordRepromptService>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UnsecuredWebsitesReportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get only unsecured ciphers that the user has "Can Edit" access to', async () => {
|
||||
const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2";
|
||||
const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3";
|
||||
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(cipherData));
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.ciphers.length).toEqual(2);
|
||||
expect(component.ciphers[0].id).toEqual(expectedIdOne);
|
||||
expect(component.ciphers[0].edit).toEqual(true);
|
||||
expect(component.ciphers[1].id).toEqual(expectedIdTwo);
|
||||
expect(component.ciphers[1].edit).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-unsecured-websites-report",
|
||||
templateUrl: "unsecured-websites-report.component.html",
|
||||
})
|
||||
export class UnsecuredWebsitesReportComponent extends CipherReportComponent implements OnInit {
|
||||
disabled = true;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const unsecuredCiphers = allCiphers.filter((c) => {
|
||||
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0);
|
||||
});
|
||||
this.ciphers = unsecuredCiphers.filter(
|
||||
(c) => (!this.organization && c.edit) || (this.organization && !c.edit),
|
||||
);
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ "weakPasswordsReport" | i18n }}
|
||||
<small *ngIf="hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<p>{{ "weakPasswordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="!hasLoaded && loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="mt-4" *ngIf="hasLoaded">
|
||||
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!ciphers.length">
|
||||
{{ "noWeakPasswords" | i18n }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="ciphers.length">
|
||||
<app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}">
|
||||
{{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list table-ciphers">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of ciphers">
|
||||
<td class="table-list-icon">
|
||||
<app-vault-icon [cipher]="c"></app-vault-icon>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<ng-container *ngIf="!organization || canManageCipher(c); else cantManage">
|
||||
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{
|
||||
c.name
|
||||
}}</a>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ c.name }}</span>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && c.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="c.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip"
|
||||
appStopProp
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ c.subTitle }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
[organizationId]="c.organizationId"
|
||||
[organizationName]="c.organizationId | orgNameFromId: (organizations$ | async)"
|
||||
appStopProp
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span bitBadge [variant]="passwordStrengthMap.get(c.id)[1]">
|
||||
{{ passwordStrengthMap.get(c.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
@@ -0,0 +1,82 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
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 { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { cipherData } from "./reports-ciphers.mock";
|
||||
import { WeakPasswordsReportComponent } from "./weak-passwords-report.component";
|
||||
|
||||
describe("WeakPasswordsReportComponent", () => {
|
||||
let component: WeakPasswordsReportComponent;
|
||||
let fixture: ComponentFixture<WeakPasswordsReportComponent>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [WeakPasswordsReportComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: passwordStrengthService,
|
||||
},
|
||||
{
|
||||
provide: OrganizationService,
|
||||
useValue: mock<OrganizationService>(),
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: mock<ModalService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordRepromptService,
|
||||
useValue: mock<PasswordRepromptService>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(WeakPasswordsReportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get only ciphers with weak passwords that the user has "Can Edit" access to', async () => {
|
||||
const expectedIdOne: any = "cbea34a8-bde4-46ad-9d19-b05001228ab2";
|
||||
const expectedIdTwo = "cbea34a8-bde4-46ad-9d19-b05001228cd3";
|
||||
|
||||
jest.spyOn(passwordStrengthService, "getPasswordStrength").mockReturnValue({
|
||||
password: "123",
|
||||
score: 0,
|
||||
} as any);
|
||||
jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve<any>(cipherData));
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.ciphers.length).toEqual(2);
|
||||
expect(component.ciphers[0].id).toEqual(expectedIdOne);
|
||||
expect(component.ciphers[0].edit).toEqual(true);
|
||||
expect(component.ciphers[1].id).toEqual(expectedIdTwo);
|
||||
expect(component.ciphers[1].edit).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { BadgeVariant } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-weak-passwords-report",
|
||||
templateUrl: "weak-passwords-report.component.html",
|
||||
})
|
||||
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
disabled = true;
|
||||
|
||||
private passwordStrengthCache = new Map<string, number>();
|
||||
weakPasswordCiphers: CipherView[] = [];
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected organizationService: OrganizationService,
|
||||
modalService: ModalService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
) {
|
||||
super(modalService, passwordRepromptService, organizationService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
this.findWeakPasswords(allCiphers);
|
||||
}
|
||||
|
||||
protected findWeakPasswords(ciphers: any[]): void {
|
||||
ciphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||
const cacheKey = this.getCacheKey(ciph);
|
||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i: any) => i.length >= 3);
|
||||
}
|
||||
}
|
||||
const result = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
);
|
||||
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||
}
|
||||
const score = this.passwordStrengthCache.get(cacheKey);
|
||||
if (score != null && score <= 2) {
|
||||
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
||||
this.weakPasswordCiphers.push(ciph);
|
||||
}
|
||||
});
|
||||
this.weakPasswordCiphers.sort((a, b) => {
|
||||
return (
|
||||
this.passwordStrengthCache.get(this.getCacheKey(a)) -
|
||||
this.passwordStrengthCache.get(this.getCacheKey(b))
|
||||
);
|
||||
});
|
||||
this.ciphers = [...this.weakPasswordCiphers];
|
||||
}
|
||||
|
||||
protected getAllCiphers(): Promise<CipherView[]> {
|
||||
return this.cipherService.getAllDecrypted();
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
// this will only ever be false from the org view;
|
||||
return true;
|
||||
}
|
||||
|
||||
private isUserNameNotEmpty(c: CipherView): boolean {
|
||||
return !Utils.isNullOrWhitespace(c.login.username);
|
||||
}
|
||||
|
||||
private getCacheKey(c: CipherView): string {
|
||||
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
|
||||
}
|
||||
|
||||
private scoreKey(score: number): [string, BadgeVariant] {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return ["strong", "success"];
|
||||
case 3:
|
||||
return ["good", "primary"];
|
||||
case 2:
|
||||
return ["weak", "warning"];
|
||||
default:
|
||||
return ["veryWeak", "danger"];
|
||||
}
|
||||
}
|
||||
}
|
||||
12
apps/web/src/app/tools/reports/reports-layout.component.html
Normal file
12
apps/web/src/app/tools/reports/reports-layout.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="container page-content">
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<a bitButton routerLink="./" *ngIf="!homepage">
|
||||
<i class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||
{{ "backToReports" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
26
apps/web/src/app/tools/reports/reports-layout.component.ts
Normal file
26
apps/web/src/app/tools/reports/reports-layout.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { Subscription } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: "app-reports-layout",
|
||||
templateUrl: "reports-layout.component.html",
|
||||
})
|
||||
export class ReportsLayoutComponent implements OnDestroy {
|
||||
homepage = true;
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(router: Router) {
|
||||
this.subscription = router.events
|
||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
.subscribe((event) => {
|
||||
this.homepage = (event as NavigationEnd).url == "/reports";
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
67
apps/web/src/app/tools/reports/reports-routing.module.ts
Normal file
67
apps/web/src/app/tools/reports/reports-routing.module.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuard } from "@bitwarden/angular/auth/guards";
|
||||
|
||||
import { hasPremiumGuard } from "../../core/guards/has-premium.guard";
|
||||
|
||||
import { BreachReportComponent } from "./pages/breach-report.component";
|
||||
import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component";
|
||||
import { InactiveTwoFactorReportComponent } from "./pages/inactive-two-factor-report.component";
|
||||
import { ReportsHomeComponent } from "./pages/reports-home.component";
|
||||
import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent } from "./pages/weak-passwords-report.component";
|
||||
import { ReportsLayoutComponent } from "./reports-layout.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: ReportsLayoutComponent,
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", component: ReportsHomeComponent, data: { homepage: true } },
|
||||
{
|
||||
path: "breach-report",
|
||||
component: BreachReportComponent,
|
||||
data: { titleId: "dataBreachReport" },
|
||||
},
|
||||
{
|
||||
path: "reused-passwords-report",
|
||||
component: ReusedPasswordsReportComponent,
|
||||
data: { titleId: "reusedPasswordsReport" },
|
||||
canActivate: [hasPremiumGuard()],
|
||||
},
|
||||
{
|
||||
path: "unsecured-websites-report",
|
||||
component: UnsecuredWebsitesReportComponent,
|
||||
data: { titleId: "unsecuredWebsitesReport" },
|
||||
canActivate: [hasPremiumGuard()],
|
||||
},
|
||||
{
|
||||
path: "weak-passwords-report",
|
||||
component: WeakPasswordsReportComponent,
|
||||
data: { titleId: "weakPasswordsReport" },
|
||||
canActivate: [hasPremiumGuard()],
|
||||
},
|
||||
{
|
||||
path: "exposed-passwords-report",
|
||||
component: ExposedPasswordsReportComponent,
|
||||
data: { titleId: "exposedPasswordsReport" },
|
||||
canActivate: [hasPremiumGuard()],
|
||||
},
|
||||
{
|
||||
path: "inactive-two-factor-report",
|
||||
component: InactiveTwoFactorReportComponent,
|
||||
data: { titleId: "inactive2faReport" },
|
||||
canActivate: [hasPremiumGuard()],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ReportsRoutingModule {}
|
||||
39
apps/web/src/app/tools/reports/reports.module.ts
Normal file
39
apps/web/src/app/tools/reports/reports.module.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { BreachReportComponent } from "./pages/breach-report.component";
|
||||
import { ExposedPasswordsReportComponent } from "./pages/exposed-passwords-report.component";
|
||||
import { InactiveTwoFactorReportComponent } from "./pages/inactive-two-factor-report.component";
|
||||
import { ReportsHomeComponent } from "./pages/reports-home.component";
|
||||
import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent } from "./pages/weak-passwords-report.component";
|
||||
import { ReportsLayoutComponent } from "./reports-layout.component";
|
||||
import { ReportsRoutingModule } from "./reports-routing.module";
|
||||
import { ReportsSharedModule } from "./shared";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
ReportsSharedModule,
|
||||
ReportsRoutingModule,
|
||||
OrganizationBadgeModule,
|
||||
PipesModule,
|
||||
],
|
||||
declarations: [
|
||||
BreachReportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
ReportsLayoutComponent,
|
||||
ReportsHomeComponent,
|
||||
ReusedPasswordsReportComponent,
|
||||
UnsecuredWebsitesReportComponent,
|
||||
WeakPasswordsReportComponent,
|
||||
],
|
||||
})
|
||||
export class ReportsModule {}
|
||||
57
apps/web/src/app/tools/reports/reports.ts
Normal file
57
apps/web/src/app/tools/reports/reports.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ReportBreach } from "./icons/report-breach.icon";
|
||||
import { ReportExposedPasswords } from "./icons/report-exposed-passwords.icon";
|
||||
import { ReportInactiveTwoFactor } from "./icons/report-inactive-two-factor.icon";
|
||||
import { ReportReusedPasswords } from "./icons/report-reused-passwords.icon";
|
||||
import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon";
|
||||
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
|
||||
import { ReportEntry } from "./shared";
|
||||
|
||||
export enum ReportType {
|
||||
ExposedPasswords = "exposedPasswords",
|
||||
ReusedPasswords = "reusedPasswords",
|
||||
WeakPasswords = "weakPasswords",
|
||||
UnsecuredWebsites = "unsecuredWebsites",
|
||||
Inactive2fa = "inactive2fa",
|
||||
DataBreach = "dataBreach",
|
||||
}
|
||||
|
||||
type ReportWithoutVariant = Omit<ReportEntry, "variant">;
|
||||
|
||||
export const reports: Record<ReportType, ReportWithoutVariant> = {
|
||||
[ReportType.ExposedPasswords]: {
|
||||
title: "exposedPasswordsReport",
|
||||
description: "exposedPasswordsReportDesc",
|
||||
route: "exposed-passwords-report",
|
||||
icon: ReportExposedPasswords,
|
||||
},
|
||||
[ReportType.ReusedPasswords]: {
|
||||
title: "reusedPasswordsReport",
|
||||
description: "reusedPasswordsReportDesc",
|
||||
route: "reused-passwords-report",
|
||||
icon: ReportReusedPasswords,
|
||||
},
|
||||
[ReportType.WeakPasswords]: {
|
||||
title: "weakPasswordsReport",
|
||||
description: "weakPasswordsReportDesc",
|
||||
route: "weak-passwords-report",
|
||||
icon: ReportWeakPasswords,
|
||||
},
|
||||
[ReportType.UnsecuredWebsites]: {
|
||||
title: "unsecuredWebsitesReport",
|
||||
description: "unsecuredWebsitesReportDesc",
|
||||
route: "unsecured-websites-report",
|
||||
icon: ReportUnsecuredWebsites,
|
||||
},
|
||||
[ReportType.Inactive2fa]: {
|
||||
title: "inactive2faReport",
|
||||
description: "inactive2faReportDesc",
|
||||
route: "inactive-two-factor-report",
|
||||
icon: ReportInactiveTwoFactor,
|
||||
},
|
||||
[ReportType.DataBreach]: {
|
||||
title: "dataBreachReport",
|
||||
description: "breachDesc",
|
||||
route: "breach-report",
|
||||
icon: ReportBreach,
|
||||
},
|
||||
};
|
||||
3
apps/web/src/app/tools/reports/shared/index.ts
Normal file
3
apps/web/src/app/tools/reports/shared/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./models/report-entry";
|
||||
export * from "./models/report-variant";
|
||||
export * from "./reports-shared.module";
|
||||
11
apps/web/src/app/tools/reports/shared/models/report-entry.ts
Normal file
11
apps/web/src/app/tools/reports/shared/models/report-entry.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Icon } from "@bitwarden/components";
|
||||
|
||||
import { ReportVariant } from "./report-variant";
|
||||
|
||||
export type ReportEntry = {
|
||||
title: string;
|
||||
description: string;
|
||||
route: string;
|
||||
icon: Icon;
|
||||
variant: ReportVariant;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum ReportVariant {
|
||||
Enabled = "Enabled",
|
||||
RequiresPremium = "RequiresPremium",
|
||||
RequiresUpgrade = "RequiresUpgrade",
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<a
|
||||
class="tw-block tw-h-full tw-max-w-72 tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 !tw-text-main tw-transition-all hover:tw-scale-105 hover:tw-no-underline focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2"
|
||||
[routerLink]="route"
|
||||
>
|
||||
<div class="tw-relative">
|
||||
<div
|
||||
class="tw-flex tw-h-28 tw-bg-background-alt2 tw-text-center tw-text-primary-300"
|
||||
[ngClass]="{ 'tw-grayscale': disabled }"
|
||||
>
|
||||
<div class="tw-m-auto"><bit-icon [icon]="icon" aria-hidden="true"></bit-icon></div>
|
||||
</div>
|
||||
<div class="tw-p-5" [ngClass]="{ 'tw-grayscale': disabled }">
|
||||
<h3 class="tw-mb-4 tw-text-xl tw-font-bold">{{ title }}</h3>
|
||||
<p class="tw-mb-0">{{ description }}</p>
|
||||
</div>
|
||||
<span
|
||||
bitBadge
|
||||
[variant]="requiresPremium ? 'success' : 'primary'"
|
||||
class="tw-absolute tw-left-2 tw-top-2 tw-leading-none"
|
||||
*ngIf="disabled"
|
||||
>
|
||||
<ng-container *ngIf="requiresPremium">{{ "premium" | i18n }}</ng-container>
|
||||
<ng-container *ngIf="!requiresPremium">{{ "upgrade" | i18n }}</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { Icon } from "@bitwarden/components";
|
||||
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
|
||||
@Component({
|
||||
selector: "app-report-card",
|
||||
templateUrl: "report-card.component.html",
|
||||
})
|
||||
export class ReportCardComponent {
|
||||
@Input() title: string;
|
||||
@Input() description: string;
|
||||
@Input() route: string;
|
||||
@Input() icon: Icon;
|
||||
@Input() variant: ReportVariant;
|
||||
|
||||
protected get disabled() {
|
||||
return this.variant != ReportVariant.Enabled;
|
||||
}
|
||||
|
||||
protected get requiresPremium() {
|
||||
return this.variant == ReportVariant.RequiresPremium;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, Story, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BadgeModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../core/tests";
|
||||
import { PremiumBadgeComponent } from "../../../../vault/components/premium-badge.component";
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
|
||||
import { ReportCardComponent } from "./report-card.component";
|
||||
|
||||
export default {
|
||||
title: "Web/Reports/Card",
|
||||
component: ReportCardComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule],
|
||||
declarations: [PremiumBadgeComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
title: "Exposed Passwords",
|
||||
description:
|
||||
"Passwords exposed in a data breach are easy targets for attackers. Change these passwords to prevent potential break-ins.",
|
||||
icon: "reportExposedPasswords",
|
||||
variant: ReportVariant.Enabled,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ReportCardComponent> = (args: ReportCardComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
|
||||
export const Enabled = Template.bind({});
|
||||
|
||||
export const RequiresPremium = Template.bind({});
|
||||
RequiresPremium.args = {
|
||||
variant: ReportVariant.RequiresPremium,
|
||||
};
|
||||
|
||||
export const RequiresUpgrade = Template.bind({});
|
||||
RequiresUpgrade.args = {
|
||||
variant: ReportVariant.RequiresUpgrade,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="tw-inline-grid tw-grid-cols-3 tw-gap-4">
|
||||
<div *ngFor="let report of reports">
|
||||
<app-report-card
|
||||
[title]="report.title | i18n"
|
||||
[description]="report.description | i18n"
|
||||
[route]="report.route"
|
||||
[variant]="report.variant"
|
||||
[icon]="report.icon"
|
||||
></app-report-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ReportEntry } from "../models/report-entry";
|
||||
|
||||
@Component({
|
||||
selector: "app-report-list",
|
||||
templateUrl: "report-list.component.html",
|
||||
})
|
||||
export class ReportListComponent {
|
||||
@Input() reports: ReportEntry[];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, Story, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BadgeModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../core/tests";
|
||||
import { PremiumBadgeComponent } from "../../../../vault/components/premium-badge.component";
|
||||
import { reports } from "../../reports";
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
import { ReportCardComponent } from "../report-card/report-card.component";
|
||||
|
||||
import { ReportListComponent } from "./report-list.component";
|
||||
|
||||
export default {
|
||||
title: "Web/Reports/List",
|
||||
component: ReportListComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule],
|
||||
declarations: [PremiumBadgeComponent, ReportCardComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
reports: Object.values(reports).map((report) => ({
|
||||
...report,
|
||||
variant:
|
||||
report.route == "breach-report" ? ReportVariant.Enabled : ReportVariant.RequiresPremium,
|
||||
})),
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ReportListComponent> = (args: ReportListComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
|
||||
import { ReportCardComponent } from "./report-card/report-card.component";
|
||||
import { ReportListComponent } from "./report-list/report-list.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule],
|
||||
declarations: [ReportCardComponent, ReportListComponent],
|
||||
exports: [ReportCardComponent, ReportListComponent],
|
||||
})
|
||||
export class ReportsSharedModule {}
|
||||
Reference in New Issue
Block a user