1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[SM-108] Refactor Reports - Add storybook stories (#3204)

This commit is contained in:
Oscar Hinton
2022-08-03 21:40:04 +02:00
committed by GitHub
parent 4398467368
commit 6b1652e34c
57 changed files with 512 additions and 223 deletions

View File

@@ -6,7 +6,7 @@
"no-restricted-imports": [
"error",
{
"patterns": ["**/core/*"]
"patterns": ["**/core/*", "**/reports/*"]
}
]
}

View File

@@ -9,7 +9,7 @@ import { EventResponse } from "@bitwarden/common/models/response/eventResponse";
import { ListResponse } from "@bitwarden/common/models/response/listResponse";
import { EventView } from "@bitwarden/common/models/view/eventView";
import { EventService } from "src/app/core";
import { EventService } from "../core";
@Directive()
export abstract class BaseEventsComponent {

View File

@@ -79,15 +79,6 @@ import { AttachmentsComponent as OrgAttachmentsComponent } from "../organization
import { CiphersComponent as OrgCiphersComponent } from "../organizations/vault/ciphers.component";
import { CollectionsComponent as OrgCollectionsComponent } from "../organizations/vault/collections.component";
import { ProvidersComponent } from "../providers/providers.component";
import { BreachReportComponent } from "../reports/breach-report.component";
import { ExposedPasswordsReportComponent } from "../reports/exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent } from "../reports/inactive-two-factor-report.component";
import { ReportCardComponent } from "../reports/report-card.component";
import { ReportListComponent } from "../reports/report-list.component";
import { ReportsComponent } from "../reports/reports.component";
import { ReusedPasswordsReportComponent } from "../reports/reused-passwords-report.component";
import { UnsecuredWebsitesReportComponent } from "../reports/unsecured-websites-report.component";
import { WeakPasswordsReportComponent } from "../reports/weak-passwords-report.component";
import { AccessComponent } from "../send/access.component";
import { AddEditComponent as SendAddEditComponent } from "../send/add-edit.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "../send/efflux-dates.component";
@@ -190,7 +181,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
AttachmentsComponent,
BillingSyncApiKeyComponent,
BillingSyncKeyComponent,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
@@ -216,13 +206,11 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditComponent,
ExposedPasswordsReportComponent,
FamiliesForEnterpriseSetupComponent,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
HintComponent,
InactiveTwoFactorReportComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
@@ -281,12 +269,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
ReportCardComponent,
ReportListComponent,
ReportsComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
ReusedPasswordsReportComponent,
SecurityComponent,
SecurityKeysComponent,
SendAddEditComponent,
@@ -313,7 +297,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UpdatePasswordComponent,
@@ -326,7 +309,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
VerifyEmailComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
],
exports: [
PremiumBadgeComponent,
@@ -343,7 +325,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
AdjustSubscription,
ApiKeyComponent,
AttachmentsComponent,
BreachReportComponent,
BulkActionsComponent,
BulkDeleteComponent,
BulkMoveComponent,
@@ -369,13 +350,11 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditComponent,
ExposedPasswordsReportComponent,
FamiliesForEnterpriseSetupComponent,
FolderAddEditComponent,
FooterComponent,
FrontendLayoutComponent,
HintComponent,
InactiveTwoFactorReportComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
@@ -433,12 +412,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
ReportCardComponent,
ReportListComponent,
ReportsComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
ReusedPasswordsReportComponent,
SecurityComponent,
SecurityKeysComponent,
SendAddEditComponent,
@@ -465,7 +440,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
UnsecuredWebsitesReportComponent,
UpdateKeyComponent,
UpdateLicenseComponent,
UpdatePasswordComponent,
@@ -478,7 +452,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
VerifyEmailComponent,
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
],
})
export class LooseComponentsModule {}

View File

@@ -64,6 +64,7 @@ import {
FormFieldModule,
SubmitButtonModule,
MenuModule,
IconModule,
} from "@bitwarden/components";
import { PaymentComponent } from "../settings/payment.component";
@@ -139,6 +140,7 @@ registerLocaleData(localeZhTw, "zh-TW");
MenuModule,
FormFieldModule,
SubmitButtonModule,
IconModule,
],
exports: [
CommonModule,
@@ -159,6 +161,7 @@ registerLocaleData(localeZhTw, "zh-TW");
SubmitButtonModule,
PaymentComponent,
TaxInfoComponent,
IconModule,
],
providers: [DatePipe],
bootstrap: [],

View File

@@ -13,7 +13,7 @@ import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { ProductType } from "@bitwarden/common/enums/productType";
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
@Component({
selector: "app-billing",

View File

@@ -17,7 +17,7 @@ import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationConnectionResponse } from "@bitwarden/common/models/response/organizationConnectionResponse";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/models/response/organizationSubscriptionResponse";
import { BillingSyncKeyComponent } from "src/app/settings/billing-sync-key.component";
import { BillingSyncKeyComponent } from "../../settings/billing-sync-key.component";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";

View File

@@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { Params } from "@angular/router";
import { BaseAcceptComponent } from "src/app/common/base.accept.component";
import { BaseAcceptComponent } from "../../common/base.accept.component";
@Component({
selector: "app-accept-family-sponsorship",

View File

@@ -15,8 +15,8 @@ import { ProductType } from "@bitwarden/common/enums/productType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/models/request/organization/organizationSponsorshipRedeemRequest";
import { DeleteOrganizationComponent } from "src/app/organizations/settings/delete-organization.component";
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
import { DeleteOrganizationComponent } from "../settings/delete-organization.component";
@Component({
selector: "families-for-enterprise-setup",

View File

@@ -11,11 +11,12 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/exposed-passwords-report.component";
// eslint-disable-next-line no-restricted-imports
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/pages/exposed-passwords-report.component";
@Component({
selector: "app-org-exposed-passwords-report",
templateUrl: "../../reports/exposed-passwords-report.component.html",
templateUrl: "../../reports/pages/exposed-passwords-report.component.html",
})
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
manageableCiphers: Cipher[];

View File

@@ -10,11 +10,12 @@ import { PasswordRepromptService } from "@bitwarden/common/abstractions/password
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../../reports/inactive-two-factor-report.component";
// eslint-disable-next-line no-restricted-imports
import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../../reports/pages/inactive-two-factor-report.component";
@Component({
selector: "app-inactive-two-factor-report",
templateUrl: "../../reports/inactive-two-factor-report.component.html",
templateUrl: "../../reports/pages/inactive-two-factor-report.component.html",
})
export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent {
constructor(

View File

@@ -10,11 +10,12 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } from "../../reports/reused-passwords-report.component";
// eslint-disable-next-line no-restricted-imports
import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } from "../../reports/pages/reused-passwords-report.component";
@Component({
selector: "app-reused-passwords-report",
templateUrl: "../../reports/reused-passwords-report.component.html",
templateUrl: "../../reports/pages/reused-passwords-report.component.html",
})
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
manageableCiphers: Cipher[];

View File

@@ -9,11 +9,12 @@ import { PasswordRepromptService } from "@bitwarden/common/abstractions/password
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent } from "../../reports/unsecured-websites-report.component";
// eslint-disable-next-line no-restricted-imports
import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent } from "../../reports/pages/unsecured-websites-report.component";
@Component({
selector: "app-unsecured-websites-report",
templateUrl: "../../reports/unsecured-websites-report.component.html",
templateUrl: "../../reports/pages/unsecured-websites-report.component.html",
})
export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent {
constructor(

View File

@@ -11,11 +11,12 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Cipher } from "@bitwarden/common/models/domain/cipher";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from "../../reports/weak-passwords-report.component";
// eslint-disable-next-line no-restricted-imports
import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from "../../reports/pages/weak-passwords-report.component";
@Component({
selector: "app-weak-passwords-report",
templateUrl: "../../reports/weak-passwords-report.component.html",
templateUrl: "../../reports/pages/weak-passwords-report.component.html",
})
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
manageableCiphers: Cipher[];

View File

@@ -31,7 +31,7 @@ import { IndividualVaultModule } from "./modules/vault/modules/individual-vault/
import { OrganizationsRoutingModule } from "./organizations/organization-routing.module";
import { AcceptFamilySponsorshipComponent } from "./organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "./organizations/sponsorships/families-for-enterprise-setup.component";
import { ReportsRoutingModule } from "./reports/reports-routing.module";
import { ReportsModule } from "./reports";
import { AccessComponent } from "./send/access.component";
import { SendComponent } from "./send/send.component";
import { AccountComponent } from "./settings/account.component";
@@ -238,7 +238,7 @@ const routes: Routes = [
},
{
path: "reports",
loadChildren: () => ReportsRoutingModule,
loadChildren: () => ReportsModule,
},
{ path: "setup/families-for-enterprise", component: FamiliesForEnterpriseSetupComponent },
],

View File

@@ -0,0 +1,3 @@
export * from "./reports.module";
export * from "./models/report-entry";
export * from "./models/report-variant";

View File

@@ -0,0 +1,9 @@
import { ReportVariant } from "./report-variant";
export type ReportEntry = {
title: string;
description: string;
route: string;
icon: string;
variant: ReportVariant;
};

View File

@@ -0,0 +1,5 @@
export enum ReportVariant {
Enabled = "Enabled",
RequiresPremium = "RequiresPremium",
RequiresUpgrade = "RequiresUpgrade",
}

View File

@@ -8,8 +8,8 @@ import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { AddEditComponent as OrgAddEditComponent } from "../organizations/vault/add-edit.component";
import { AddEditComponent } from "../vault/add-edit.component";
import { AddEditComponent as OrgAddEditComponent } from "../../organizations/vault/add-edit.component";
import { AddEditComponent } from "../../vault/add-edit.component";
@Directive()
export class CipherReportComponent {
@@ -79,13 +79,6 @@ export class CipherReportComponent {
this.messagingService.send("upgradeOrganization", { organizationId: this.organization.id });
return false;
}
} else {
const accessPremium = await this.stateService.getCanAccessPremium();
if (this.requiresPaid && !accessPremium) {
this.messagingService.send("premiumRequired");
this.loading = false;
return false;
}
}
return true;
}

View File

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

View File

@@ -0,0 +1,52 @@
import { Component, OnInit } from "@angular/core";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ReportEntry } from "../models/report-entry";
import { ReportVariant } from "../models/report-variant";
import { reports, ReportType } from "../reports";
@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,
},
];
}
}

View File

@@ -1,166 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
export enum ReportTypes {
"exposedPasswords" = "exposedPasswords",
"reusedPasswords" = "reusedPasswords",
"weakPasswords" = "weakPasswords",
"unsecuredWebsites" = "unsecuredWebsites",
"inactive2fa" = "inactive2fa",
"dataBreach" = "dataBreach",
}
type ReportEntry = {
title: string;
description: string;
route: string;
icon: string;
requiresPremium: boolean;
};
const reports: Record<ReportTypes, ReportEntry> = {
exposedPasswords: {
title: "exposedPasswordsReport",
description: "exposedPasswordsReportDesc",
route: "exposed-passwords-report",
icon: `
<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>
`,
requiresPremium: true,
},
reusedPasswords: {
title: "reusedPasswordsReport",
description: "reusedPasswordsReportDesc",
route: "reused-passwords-report",
icon: `
<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>
`,
requiresPremium: true,
},
weakPasswords: {
title: "weakPasswordsReport",
description: "weakPasswordsReportDesc",
route: "weak-passwords-report",
icon: `
<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>
`,
requiresPremium: true,
},
unsecuredWebsites: {
title: "unsecuredWebsitesReport",
description: "unsecuredWebsitesReportDesc",
route: "unsecured-websites-report",
icon: `
<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>
`,
requiresPremium: true,
},
inactive2fa: {
title: "inactive2faReport",
description: "inactive2faReportDesc",
route: "inactive-two-factor-report",
icon: `
<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>
`,
requiresPremium: true,
},
dataBreach: {
title: "dataBreachReport",
description: "breachDesc",
route: "breach-report",
icon: `
<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>
`,
requiresPremium: false,
},
};
@Component({
selector: "app-report-card",
templateUrl: "report-card.component.html",
})
export class ReportCardComponent implements OnInit {
@Input() type: ReportTypes;
report: ReportEntry;
hasPremium: boolean;
constructor(
private stateService: StateService,
private messagingService: MessagingService,
private sanitizer: DomSanitizer
) {}
async ngOnInit() {
this.report = reports[this.type];
this.hasPremium = await this.stateService.getCanAccessPremium();
}
get premium() {
return this.report.requiresPremium && !this.hasPremium;
}
get route() {
if (this.premium) {
return null;
}
return this.report.route;
}
get icon() {
return this.sanitizer.bypassSecurityTrustHtml(this.report.icon);
}
click() {
if (this.premium) {
this.messagingService.send("premiumRequired");
}
}
}

View File

@@ -1,25 +1,26 @@
<a
class="tw-block tw-h-full tw-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"
(click)="click()"
>
<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': premium }"
[ngClass]="{ 'tw-grayscale': disabled }"
>
<div class="tw-m-auto" [innerHtml]="icon"></div>
<div class="tw-m-auto"><bit-icon [icon]="icon"></bit-icon></div>
</div>
<div class="tw-p-5" [ngClass]="{ 'tw-grayscale': report.requiresPremium }">
<h3 class="tw-mb-4 tw-text-xl tw-font-bold">{{ report.title | i18n }}</h3>
<p class="tw-mb-0">{{ report.description | i18n }}</p>
<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
badgeType="success"
class="tw-absolute tw-left-2 tw-top-2 tw-leading-none"
*ngIf="premium"
>{{ "premium" | i18n }}</span
*ngIf="disabled"
>
<ng-container *ngIf="requiresPremium">{{ "premium" | i18n }}</ng-container>
<ng-container *ngIf="!requiresPremium">{{ "upgrade" | i18n }}</ng-container>
</span>
</div>
</a>

View File

@@ -0,0 +1,23 @@
import { Component, Input } from "@angular/core";
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: string;
@Input() variant: ReportVariant;
protected get disabled() {
return this.variant != ReportVariant.Enabled;
}
protected get requiresPremium() {
return this.variant == ReportVariant.RequiresPremium;
}
}

View File

@@ -0,0 +1,51 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, Story, moduleMetadata } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BadgeModule, IconModule } from "@bitwarden/components";
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
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,
PreloadedEnglishI18nModule,
],
declarations: [PremiumBadgeComponent],
}),
],
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,
};

View File

@@ -1,11 +0,0 @@
<div class="page-header">
<h1>{{ "reports" | i18n }}</h1>
</div>
<p>{{ "reportsDesc" | i18n }}</p>
<div class="tw-inline-grid tw-grid-cols-3 tw-gap-4">
<div *ngFor="let report of reports">
<app-report-card [type]="report"></app-report-card>
</div>
</div>

View File

@@ -1,18 +0,0 @@
import { Component } from "@angular/core";
import { ReportTypes } from "./report-card.component";
@Component({
selector: "app-report-list",
templateUrl: "report-list.component.html",
})
export class ReportListComponent {
reports = [
ReportTypes.exposedPasswords,
ReportTypes.reusedPasswords,
ReportTypes.weakPasswords,
ReportTypes.unsecuredWebsites,
ReportTypes.inactive2fa,
ReportTypes.dataBreach,
];
}

View File

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

View File

@@ -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[];
}

View File

@@ -0,0 +1,43 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, Story, moduleMetadata } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BadgeModule, IconModule } from "@bitwarden/components";
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
import { ReportVariant } from "../models/report-variant";
import { ReportCardComponent } from "../report-card/report-card.component";
import { reports } from "../reports";
import { ReportListComponent } from "./report-list.component";
export default {
title: "Web/Reports/List",
component: ReportListComponent,
decorators: [
moduleMetadata({
imports: [
JslibModule,
BadgeModule,
RouterTestingModule,
PreloadedEnglishI18nModule,
IconModule,
],
declarations: [PremiumBadgeComponent, ReportCardComponent],
}),
],
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({});

View File

@@ -4,10 +4,10 @@ import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
@Component({
selector: "app-reports",
templateUrl: "reports.component.html",
selector: "app-reports-layout",
templateUrl: "reports-layout.component.html",
})
export class ReportsComponent implements OnDestroy {
export class ReportsLayoutComponent implements OnDestroy {
homepage = true;
subscription: Subscription;

View File

@@ -3,22 +3,24 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import { BreachReportComponent } from "./breach-report.component";
import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent } from "./inactive-two-factor-report.component";
import { ReportListComponent } from "./report-list.component";
import { ReportsComponent } from "./reports.component";
import { ReusedPasswordsReportComponent } from "./reused-passwords-report.component";
import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.component";
import { WeakPasswordsReportComponent } from "./weak-passwords-report.component";
import { HasPremiumGuard } from "../shared/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: ReportsComponent,
component: ReportsLayoutComponent,
canActivate: [AuthGuard],
children: [
{ path: "", pathMatch: "full", component: ReportListComponent, data: { homepage: true } },
{ path: "", pathMatch: "full", component: ReportsHomeComponent, data: { homepage: true } },
{
path: "breach-report",
component: BreachReportComponent,
@@ -28,26 +30,31 @@ const routes: Routes = [
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],
},
],
},

View File

@@ -0,0 +1,34 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { SharedModule } from "../modules/shared.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 { ReportCardComponent } from "./report-card/report-card.component";
import { ReportListComponent } from "./report-list/report-list.component";
import { ReportsLayoutComponent } from "./reports-layout.component";
import { ReportsRoutingModule } from "./reports-routing.module";
@NgModule({
imports: [CommonModule, SharedModule, ReportsRoutingModule],
declarations: [
BreachReportComponent,
ExposedPasswordsReportComponent,
InactiveTwoFactorReportComponent,
ReportCardComponent,
ReportListComponent,
ReportsLayoutComponent,
ReportsHomeComponent,
ReusedPasswordsReportComponent,
UnsecuredWebsitesReportComponent,
WeakPasswordsReportComponent,
WeakPasswordsReportComponent,
],
})
export class ReportsModule {}

View File

@@ -0,0 +1,51 @@
import { ReportEntry } from "./models/report-entry";
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",
},
};

View File

@@ -7,7 +7,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
import { ThemeType } from "@bitwarden/common/enums/themeType";
import ThemeVariables from "src/scss/export.module.scss";
import ThemeVariables from "../../scss/export.module.scss";
const lightInputColor = ThemeVariables.lightInputColor;
const lightInputPlaceholderColor = ThemeVariables.lightInputPlaceholderColor;

View File

@@ -0,0 +1,31 @@
import { Injectable } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from "@angular/router";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
@Injectable({
providedIn: "root",
})
export class HasPremiumGuard implements CanActivate {
constructor(
private router: Router,
private stateService: StateService,
private messagingService: MessagingService
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const userHasPremium = await this.stateService.getCanAccessPremium();
if (!userHasPremium) {
this.messagingService.send("premiumRequired");
}
// Prevent trapping the user on the login page, since that's an awful UX flow
if (!userHasPremium && this.router.url === "/login") {
return this.router.createUrlTree(["/"]);
}
return userHasPremium;
}
}

View File

@@ -0,0 +1,38 @@
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { I18nService as BaseI18nService } from "@bitwarden/common/services/i18n.service";
import * as eng from "../../locales/en/messages.json";
class PreloadedEnglishI18nService extends BaseI18nService {
constructor() {
super("en", "", () => {
return Promise.resolve(eng);
});
}
}
function i18nInitializer(i18nService: I18nService): () => Promise<void> {
return async () => {
await (i18nService as any).init();
};
}
// This is a helper I18nService implementation that loads the english `message.json` eliminating
// the need for fetching them dynamically. It should only be used within storybook.
@NgModule({
providers: [
{
provide: I18nService,
useClass: PreloadedEnglishI18nService,
},
{
provide: APP_INITIALIZER,
useFactory: i18nInitializer,
deps: [I18nService],
multi: true,
},
],
})
export class PreloadedEnglishI18nModule {}

View File

@@ -3,6 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"module": "ES2020",
"resolveJsonModule": true,
"paths": {
"tldjs": ["../../libs/common/src/misc/tldjs.noop"],
"src/*": ["src/*"],