[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,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/*"],

View File

@ -0,0 +1,24 @@
import { Component, HostBinding, Input } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { Icon, IconSvg } from "./icons";
@Component({
selector: "bit-icon",
template: ``,
})
export class BitIconComponent {
@Input() icon: Icon;
constructor(private domSanitizer: DomSanitizer) {}
@HostBinding("innerHtml")
protected get innerHtml() {
const svg = IconSvg[this.icon];
if (svg == null) {
return "Unknown icon";
}
return this.domSanitizer.bypassSecurityTrustHtml(IconSvg[this.icon]);
}
}

View File

@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BitIconComponent } from "./icon.component";
@NgModule({
imports: [CommonModule],
declarations: [BitIconComponent],
exports: [BitIconComponent],
})
export class IconModule {}

View File

@ -0,0 +1,27 @@
import { Meta, Story } from "@storybook/angular";
import { BitIconComponent } from "./icon.component";
export default {
title: "Component Library/Icon",
component: BitIconComponent,
args: {
icon: "reportExposedPasswords",
},
} as Meta;
const Template: Story<BitIconComponent> = (args: BitIconComponent) => ({
props: args,
template: `
<div class="tw-bg-primary-500 tw-p-5">
<bit-icon [icon]="icon" class="tw-text-primary-300"></bit-icon>
</div>
`,
});
export const ReportExposedPasswords = Template.bind({});
export const UnknownIcon = Template.bind({});
UnknownIcon.args = {
icon: "unknown",
};

View File

@ -1,32 +1,5 @@
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: `
export const IconSvg = {
reportExposedPasswords: `
<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" />
@ -40,26 +13,14 @@ const reports: Record<ReportTypes, ReportEntry> = {
<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: `
reportReusedPasswords: `
<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: `
reportWeakPasswords: `
<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" />
@ -67,13 +28,7 @@ const reports: Record<ReportTypes, ReportEntry> = {
<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: `
reportUnsecuredWebsites: `
<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" />
@ -83,13 +38,7 @@ const reports: Record<ReportTypes, ReportEntry> = {
<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: `
reportInactiveTwoFactor: `
<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" />
@ -97,13 +46,7 @@ const reports: Record<ReportTypes, ReportEntry> = {
<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: `
reportBreach: `
<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" />
@ -115,52 +58,6 @@ const reports: Record<ReportTypes, ReportEntry> = {
<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");
}
}
}
export type Icon = keyof typeof IconSvg;

View File

@ -0,0 +1 @@
export * from "./icon.module";

View File

@ -1,11 +1,12 @@
export * from "./badge";
export * from "./banner";
export * from "./button";
export * from "./toggle-group";
export * from "./callout";
export * from "./form-field";
export * from "./icon";
export * from "./menu";
export * from "./modal";
export * from "./utils/i18n-mock.service";
export * from "./tabs";
export * from "./submit-button";
export * from "./tabs";
export * from "./toggle-group";
export * from "./utils/i18n-mock.service";

View File

@ -1,7 +1,11 @@
/* eslint-disable */
const config = require("./libs/components/tailwind.config.base");
config.content = ["./libs/components/src/**/*.{html,ts,mdx}", "./.storybook/preview.js"];
config.content = [
"./libs/components/src/**/*.{html,ts,mdx}",
"./apps/web/src/**/*.{html,ts,mdx}",
"./.storybook/preview.js",
];
config.safelist = [
{
pattern: /tw-bg-(.*)/,

View File

@ -13,6 +13,7 @@
"declaration": false,
"outDir": "dist",
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@bitwarden/common/*": ["./libs/common/src/*"],
"@bitwarden/angular/*": ["./libs/angular/src/*"],
@ -26,6 +27,6 @@
}
]
},
"include": ["apps/*/src/**/*.stories.ts", "libs/*/src/**/*"],
"exclude": ["apps/*/src/**/*.spec.ts", "libs/*/src/**/*.spec.ts"]
"include": ["apps/web/src/**/*", "libs/*/src/**/*"],
"exclude": ["apps/web/src/**/*.spec.ts", "libs/*/src/**/*.spec.ts"]
}