mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-21 21:11:35 +01:00
[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:
parent
a462e93a64
commit
b79554a13b
@ -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;
|
||||
}
|
||||
}
|
@ -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],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -139,8 +139,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.router.navigate([
|
||||
"organizations",
|
||||
message.organizationId,
|
||||
"settings",
|
||||
"billing",
|
||||
"subscription",
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
@ -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 = [];
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -30,9 +30,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
|
@ -28,9 +28,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
|
@ -24,9 +24,7 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
|
@ -31,9 +31,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (await this.checkAccess()) {
|
||||
await super.load();
|
||||
}
|
||||
await super.load();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -6673,5 +6673,8 @@
|
||||
},
|
||||
"dismiss": {
|
||||
"message": "Dismiss"
|
||||
},
|
||||
"notAvailableForFreeOrganization": {
|
||||
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user