57 changed files with 512 additions and 223 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from "./reports.module"; |
||||
export * from "./models/report-entry"; |
||||
export * from "./models/report-variant"; |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import { ReportVariant } from "./report-variant"; |
||||
|
||||
export type ReportEntry = { |
||||
title: string; |
||||
description: string; |
||||
route: string; |
||||
icon: string; |
||||
variant: ReportVariant; |
||||
}; |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
export enum ReportVariant { |
||||
Enabled = "Enabled", |
||||
RequiresPremium = "RequiresPremium", |
||||
RequiresUpgrade = "RequiresUpgrade", |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -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,52 @@
@@ -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, |
||||
}, |
||||
]; |
||||
} |
||||
} |
||||
@ -1,25 +1,26 @@
@@ -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> |
||||
@ -0,0 +1,23 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,51 @@
@@ -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, |
||||
}; |
||||
@ -1,11 +0,0 @@
@@ -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> |
||||
@ -1,18 +0,0 @@
@@ -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, |
||||
]; |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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,43 @@
@@ -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({}); |
||||
@ -0,0 +1,34 @@
@@ -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 {} |
||||
@ -0,0 +1,51 @@
@@ -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", |
||||
}, |
||||
}; |
||||
@ -0,0 +1,31 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -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 {} |
||||
@ -0,0 +1,24 @@
@@ -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]); |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -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 {} |
||||
@ -0,0 +1,27 @@
@@ -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", |
||||
}; |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from "./icon.module"; |
||||
@ -1,11 +1,12 @@
@@ -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"; |
||||
|
||||
Loading…
Reference in new issue