[PM-283] Fix Reports UI behavior for premium and free users (#4926)

* Prevent rerouting to dispaly modal message, and refactored components where thsi was used

* Added upgrade badge to organization reports view

* created guard to prevent free organization users from accessing reports

* Added isUpgradeRequired getter to organization class

* Modifiewd reports home to pass upgrade badge and add new guard to organization reports module

* Fixed routing bug when routing to billing subscription page

* Refactored to use async pipe and observables

* Renamed getter name to be more descriptive

* Removed checkAccess from reports

* Renamed guard

* Removed unused variables

* Lint fix

* Lint fix

* prettier fix

* Corrected organiztion service reference

* Moved homepage to ngonInit

* [PM-1629] Update the upgrade dialog for users without billing rights (#5102)

* Show dialog with description when user does not have access to the billing page

* switched conditions to nested if to make the logic clearer
This commit is contained in:
SmithThe4th 2023-03-30 16:27:03 -04:00 committed by GitHub
parent a462e93a64
commit b79554a13b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 104 additions and 72 deletions

View File

@ -0,0 +1,44 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@Injectable({
providedIn: "root",
})
export class IsPaidOrgGuard implements CanActivate {
constructor(
private router: Router,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private i18nService: I18nService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const org = this.organizationService.get(route.params.organizationId);
if (org == null) {
return this.router.createUrlTree(["/"]);
}
if (org.isFreeOrg) {
// Users without billing permission can't access billing
if (!org.canManageBilling) {
await this.platformUtilsService.showDialog(
this.i18nService.t("notAvailableForFreeOrganization"),
this.i18nService.t("upgradeOrganization"),
this.i18nService.t("ok")
);
return false;
} else {
this.messagingService.send("upgradeOrganization", { organizationId: org.id });
}
}
return !org.isFreeOrg;
}
}

View File

@ -9,6 +9,7 @@ import { InactiveTwoFactorReportComponent } from "../../../admin-console/organiz
import { ReusedPasswordsReportComponent } from "../../../admin-console/organizations/tools/reused-passwords-report.component";
import { UnsecuredWebsitesReportComponent } from "../../../admin-console/organizations/tools/unsecured-websites-report.component";
import { WeakPasswordsReportComponent } from "../../../admin-console/organizations/tools/weak-passwords-report.component";
import { IsPaidOrgGuard } from "../guards/is-paid-org.guard";
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
import { OrganizationRedirectGuard } from "../guards/org-redirect.guard";
import { EventsComponent } from "../manage/events.component";
@ -46,6 +47,7 @@ const routes: Routes = [
data: {
titleId: "exposedPasswordsReport",
},
canActivate: [IsPaidOrgGuard],
},
{
path: "inactive-two-factor-report",
@ -53,6 +55,7 @@ const routes: Routes = [
data: {
titleId: "inactive2faReport",
},
canActivate: [IsPaidOrgGuard],
},
{
path: "reused-passwords-report",
@ -60,6 +63,7 @@ const routes: Routes = [
data: {
titleId: "reusedPasswordsReport",
},
canActivate: [IsPaidOrgGuard],
},
{
path: "unsecured-websites-report",
@ -67,6 +71,7 @@ const routes: Routes = [
data: {
titleId: "unsecuredWebsitesReport",
},
canActivate: [IsPaidOrgGuard],
},
{
path: "weak-passwords-report",
@ -74,6 +79,7 @@ const routes: Routes = [
data: {
titleId: "weakPasswordsReport",
},
canActivate: [IsPaidOrgGuard],
},
],
},

View File

@ -1,18 +1,18 @@
<ng-container *ngIf="homepage">
<ng-container *ngIf="homepage$ | async">
<div class="page-header">
<h1>{{ "reports" | i18n }}</h1>
</div>
<p>{{ "orgsReportsDesc" | i18n }}</p>
<app-report-list [reports]="reports"></app-report-list>
<app-report-list [reports]="reports$ | async"></app-report-list>
</ng-container>
<router-outlet></router-outlet>
<div class="row mt-4">
<div class="col">
<a bitButton routerLink="./" *ngIf="!homepage">
<a bitButton routerLink="./" *ngIf="!(homepage$ | async)">
<i class="bwi bwi-angle-left" aria-hidden="true"></i>
{{ "backToReports" | i18n }}
</a>

View File

@ -1,8 +1,9 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import { filter, Subject, takeUntil } from "rxjs";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { filter, map, Observable, startWith } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../reports";
@ -10,56 +11,56 @@ import { ReportVariant, reports, ReportType, ReportEntry } from "../../../report
selector: "app-org-reports-home",
templateUrl: "reports-home.component.html",
})
export class ReportsHomeComponent implements OnInit, OnDestroy {
reports: ReportEntry[];
export class ReportsHomeComponent implements OnInit {
reports$: Observable<ReportEntry[]>;
homepage$: Observable<boolean>;
homepage = true;
private destrory$: Subject<void> = new Subject<void>();
constructor(
private route: ActivatedRoute,
private stateService: StateService,
private organizationService: OrganizationService,
private router: Router
) {}
constructor(private stateService: StateService, router: Router) {
router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
takeUntil(this.destrory$)
)
.subscribe((event) => {
this.homepage = (event as NavigationEnd).urlAfterRedirects.endsWith("/reports");
});
ngOnInit() {
this.homepage$ = this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map((event) => (event as NavigationEnd).urlAfterRedirects.endsWith("/reports")),
startWith(true)
);
this.reports$ = this.route.params.pipe(
map((params) => this.organizationService.get(params.organizationId)),
map((org) => this.buildReports(org.isFreeOrg))
);
}
async ngOnInit(): Promise<void> {
const userHasPremium = await this.stateService.getCanAccessPremium();
private buildReports(upgradeRequired: boolean): ReportEntry[] {
const reportRequiresUpgrade = upgradeRequired
? ReportVariant.RequiresUpgrade
: ReportVariant.Enabled;
const reportRequiresPremium = userHasPremium
? ReportVariant.Enabled
: ReportVariant.RequiresPremium;
this.reports = [
return [
{
...reports[ReportType.ExposedPasswords],
variant: reportRequiresPremium,
variant: reportRequiresUpgrade,
},
{
...reports[ReportType.ReusedPasswords],
variant: reportRequiresPremium,
variant: reportRequiresUpgrade,
},
{
...reports[ReportType.WeakPasswords],
variant: reportRequiresPremium,
variant: reportRequiresUpgrade,
},
{
...reports[ReportType.UnsecuredWebsites],
variant: reportRequiresPremium,
variant: reportRequiresUpgrade,
},
{
...reports[ReportType.Inactive2fa],
variant: reportRequiresPremium,
variant: reportRequiresUpgrade,
},
];
}
ngOnDestroy(): void {
this.destrory$.next();
this.destrory$.complete();
}
}

View File

@ -33,12 +33,11 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
super(cipherService, auditService, modalService, messagingService, passwordRepromptService);
}
ngOnInit() {
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.organization = await this.organizationService.get(params.organizationId);
this.manageableCiphers = await this.cipherService.getAll();
await this.checkAccess();
});
}

View File

@ -139,8 +139,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate([
"organizations",
message.organizationId,
"settings",
"billing",
"subscription",
]);
}
break;

View File

@ -72,18 +72,6 @@ export class CipherReportComponent {
return childComponent;
}
protected async checkAccess(): Promise<boolean> {
if (this.organization != null) {
// TODO: Maybe we want to just make sure they are not on a free plan? Just compare useTotp for now
// since all paid plans include useTotp
if (this.requiresPaid && !this.organization.useTotp) {
this.messagingService.send("upgradeOrganization", { organizationId: this.organization.id });
return false;
}
}
return true;
}
protected async setCiphers() {
this.ciphers = [];
}

View File

@ -27,14 +27,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
super(modalService, messagingService, true, passwordRepromptService);
}
ngOnInit() {
this.checkAccess();
}
async load() {
if (await this.checkAccess()) {
super.load();
}
async ngOnInit() {
await super.load();
}
async setCiphers() {

View File

@ -30,9 +30,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
await super.load();
}
async setCiphers() {

View File

@ -28,9 +28,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
await super.load();
}
async setCiphers() {

View File

@ -24,9 +24,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
await super.load();
}
async setCiphers() {

View File

@ -31,9 +31,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
async ngOnInit() {
if (await this.checkAccess()) {
await super.load();
}
await super.load();
}
async setCiphers() {

View File

@ -15,7 +15,7 @@
</div>
<span
bitBadge
badgeType="success"
[badgeType]="requiresPremium ? 'success' : 'primary'"
class="tw-absolute tw-left-2 tw-top-2 tw-leading-none"
*ngIf="disabled"
>

View File

@ -6673,5 +6673,8 @@
},
"dismiss": {
"message": "Dismiss"
},
"notAvailableForFreeOrganization": {
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
}
}

View File

@ -210,6 +210,11 @@ export class Organization {
return this.useSecretsManager && this.accessSecretsManager;
}
get isFreeOrg() {
// return true if organization needs to be upgraded from a free org
return !this.useTotp;
}
static fromJSON(json: Jsonify<Organization>) {
if (json == null) {
return null;