mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[EC-678] [EC-673] Fix active tab not showing selected while in child route (#3964)
* [PS-1114] hide reporting sidebar if only events * [PS-1114] add orgRedirectGuard * [PS-1114] highlight tabs based on route subset * [PS-1114] redirect to correct child route on tab - Use new OrgRedirectGuard * [PS-1114] add settings redirect using guard - refactored guard to accept array of strings * [EC-678] [EC-673] remove remaining methods * [EC-678][EC-673] address PR feedback - change switch to if statements - remove ternary
This commit is contained in:
32
apps/web/src/app/organizations/guards/org-redirect.guard.ts
Normal file
32
apps/web/src/app/organizations/guards/org-redirect.guard.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
canAccessOrgAdmin,
|
||||||
|
OrganizationService,
|
||||||
|
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class OrganizationRedirectGuard implements CanActivate {
|
||||||
|
constructor(private router: Router, private organizationService: OrganizationService) {}
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
const org = this.organizationService.get(route.params.organizationId);
|
||||||
|
|
||||||
|
const customRedirect = route.data?.autoRedirectCallback;
|
||||||
|
if (customRedirect) {
|
||||||
|
let redirectPath = customRedirect(org);
|
||||||
|
if (typeof redirectPath === "string") {
|
||||||
|
redirectPath = [redirectPath];
|
||||||
|
}
|
||||||
|
return this.router.createUrlTree([state.url, ...redirectPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canAccessOrgAdmin(org)) {
|
||||||
|
return this.router.createUrlTree(["/organizations", org.id]);
|
||||||
|
}
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,10 @@
|
|||||||
></app-organization-switcher>
|
></app-organization-switcher>
|
||||||
<bit-tab-nav-bar class="-tw-mb-px">
|
<bit-tab-nav-bar class="-tw-mb-px">
|
||||||
<bit-tab-link route="vault">{{ "vault" | i18n }}</bit-tab-link>
|
<bit-tab-link route="vault">{{ "vault" | i18n }}</bit-tab-link>
|
||||||
<bit-tab-link *ngIf="canShowManageTab(organization)" [route]="getManageRoute(organization)">
|
<bit-tab-link *ngIf="canShowManageTab(organization)" route="manage">
|
||||||
{{ "manage" | i18n }}
|
{{ "manage" | i18n }}
|
||||||
</bit-tab-link>
|
</bit-tab-link>
|
||||||
<bit-tab-link
|
<bit-tab-link *ngIf="canShowReportsTab(organization)" route="reporting">
|
||||||
*ngIf="canShowReportsTab(organization)"
|
|
||||||
[route]="getReportRoute(organization)"
|
|
||||||
>
|
|
||||||
{{ getReportTabLabel(organization) | i18n }}
|
{{ getReportTabLabel(organization) | i18n }}
|
||||||
</bit-tab-link>
|
</bit-tab-link>
|
||||||
<bit-tab-link *ngIf="canShowBillingTab(organization)" route="billing">{{
|
<bit-tab-link *ngIf="canShowBillingTab(organization)" route="billing">{{
|
||||||
|
|||||||
@@ -72,24 +72,4 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
getReportTabLabel(organization: Organization): string {
|
getReportTabLabel(organization: Organization): string {
|
||||||
return organization.useEvents ? "reporting" : "reports";
|
return organization.useEvents ? "reporting" : "reports";
|
||||||
}
|
}
|
||||||
|
|
||||||
getReportRoute(organization: Organization): string {
|
|
||||||
return organization.useEvents ? "reporting/events" : "reporting/reports";
|
|
||||||
}
|
|
||||||
|
|
||||||
getManageRoute(organization: Organization): string {
|
|
||||||
let route: string;
|
|
||||||
switch (true) {
|
|
||||||
case organization.canManageUsers:
|
|
||||||
route = "manage/members";
|
|
||||||
break;
|
|
||||||
case organization.canViewAssignedCollections || organization.canViewAllCollections:
|
|
||||||
route = "manage/collections";
|
|
||||||
break;
|
|
||||||
case organization.canManageGroups:
|
|
||||||
route = "manage/groups";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
canAccessOrgAdmin,
|
canAccessOrgAdmin,
|
||||||
canManageCollections,
|
canManageCollections,
|
||||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||||
|
import { OrganizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||||
import { CollectionsComponent } from "./manage/collections.component";
|
import { CollectionsComponent } from "./manage/collections.component";
|
||||||
import { GroupsComponent } from "./manage/groups.component";
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
@@ -47,7 +49,11 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
pathMatch: "full",
|
pathMatch: "full",
|
||||||
redirectTo: "members",
|
canActivate: [OrganizationRedirectGuard],
|
||||||
|
data: {
|
||||||
|
autoRedirectCallback: getManageRoute,
|
||||||
|
},
|
||||||
|
children: [], // This is required to make the auto redirect work
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "collections",
|
path: "collections",
|
||||||
@@ -94,6 +100,19 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getManageRoute(organization: Organization): string {
|
||||||
|
if (organization.canManageUsers) {
|
||||||
|
return "members";
|
||||||
|
}
|
||||||
|
if (organization.canViewAssignedCollections || organization.canViewAllCollections) {
|
||||||
|
return "collections";
|
||||||
|
}
|
||||||
|
if (organization.canManageGroups) {
|
||||||
|
return "groups";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { canAccessReportingTab } from "@bitwarden/common/abstractions/organizati
|
|||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||||
|
import { OrganizationRedirectGuard } from "../guards/org-redirect.guard";
|
||||||
import { EventsComponent } from "../manage/events.component";
|
import { EventsComponent } from "../manage/events.component";
|
||||||
import { ExposedPasswordsReportComponent } from "../tools/exposed-passwords-report.component";
|
import { ExposedPasswordsReportComponent } from "../tools/exposed-passwords-report.component";
|
||||||
import { InactiveTwoFactorReportComponent } from "../tools/inactive-two-factor-report.component";
|
import { InactiveTwoFactorReportComponent } from "../tools/inactive-two-factor-report.component";
|
||||||
@@ -22,7 +23,15 @@ const routes: Routes = [
|
|||||||
canActivate: [OrganizationPermissionsGuard],
|
canActivate: [OrganizationPermissionsGuard],
|
||||||
data: { organizationPermissions: canAccessReportingTab },
|
data: { organizationPermissions: canAccessReportingTab },
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "reports" },
|
{
|
||||||
|
path: "",
|
||||||
|
pathMatch: "full",
|
||||||
|
canActivate: [OrganizationRedirectGuard],
|
||||||
|
data: {
|
||||||
|
autoRedirectCallback: getReportRoute,
|
||||||
|
},
|
||||||
|
children: [], // This is required to make the auto redirect work,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "reports",
|
path: "reports",
|
||||||
component: ReportsHomeComponent,
|
component: ReportsHomeComponent,
|
||||||
@@ -80,6 +89,17 @@ const routes: Routes = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getReportRoute(organization: Organization): string {
|
||||||
|
if (organization.canAccessEventLogs) {
|
||||||
|
return "events";
|
||||||
|
}
|
||||||
|
if (organization.canAccessReports) {
|
||||||
|
return "reports";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export class ReportingComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(
|
.pipe(
|
||||||
concatMap(async (params) => {
|
concatMap(async (params) => {
|
||||||
this.organization = await this.organizationService.get(params.organizationId);
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
this.showLeftNav = this.organization.canAccessEventLogs;
|
this.showLeftNav =
|
||||||
|
this.organization.canAccessEventLogs && this.organization.canAccessReports;
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { canAccessSettingsTab } from "@bitwarden/common/abstractions/organizatio
|
|||||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||||
|
import { OrganizationRedirectGuard } from "../guards/org-redirect.guard";
|
||||||
import { PoliciesComponent } from "../policies";
|
import { PoliciesComponent } from "../policies";
|
||||||
|
|
||||||
import { AccountComponent } from "./account.component";
|
import { AccountComponent } from "./account.component";
|
||||||
@@ -18,7 +19,15 @@ const routes: Routes = [
|
|||||||
canActivate: [OrganizationPermissionsGuard],
|
canActivate: [OrganizationPermissionsGuard],
|
||||||
data: { organizationPermissions: canAccessSettingsTab },
|
data: { organizationPermissions: canAccessSettingsTab },
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "account" },
|
{
|
||||||
|
path: "",
|
||||||
|
pathMatch: "full",
|
||||||
|
canActivate: [OrganizationRedirectGuard],
|
||||||
|
data: {
|
||||||
|
autoRedirectCallback: getSettingsRoute,
|
||||||
|
},
|
||||||
|
children: [], // This is required to make the auto redirect work,
|
||||||
|
},
|
||||||
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
||||||
{
|
{
|
||||||
path: "two-factor",
|
path: "two-factor",
|
||||||
@@ -45,6 +54,25 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function getSettingsRoute(organization: Organization) {
|
||||||
|
if (organization.isOwner) {
|
||||||
|
return "account";
|
||||||
|
}
|
||||||
|
if (organization.canManagePolicies) {
|
||||||
|
return "policies";
|
||||||
|
}
|
||||||
|
if (organization.canAccessImportExport) {
|
||||||
|
return ["tools", "import"];
|
||||||
|
}
|
||||||
|
if (organization.canManageSso) {
|
||||||
|
return "sso";
|
||||||
|
}
|
||||||
|
if (organization.canManageScim) {
|
||||||
|
return "scim";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">{{ "settings" | i18n }}</div>
|
<div class="card-header">{{ "settings" | i18n }}</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
<a
|
||||||
|
routerLink="account"
|
||||||
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="organization?.isOwner"
|
||||||
|
>
|
||||||
{{ "organizationInfo" | i18n }}
|
{{ "organizationInfo" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -19,7 +24,7 @@
|
|||||||
routerLink="two-factor"
|
routerLink="two-factor"
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
*ngIf="organization?.use2fa"
|
*ngIf="organization?.use2fa && organization?.isOwner"
|
||||||
>
|
>
|
||||||
{{ "twoStepLogin" | i18n }}
|
{{ "twoStepLogin" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ export function canAccessVaultTab(org: Organization): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function canAccessSettingsTab(org: Organization): boolean {
|
export function canAccessSettingsTab(org: Organization): boolean {
|
||||||
return org.isOwner;
|
return (
|
||||||
|
org.isOwner ||
|
||||||
|
org.canManagePolicies ||
|
||||||
|
org.canManageSso ||
|
||||||
|
org.canManageScim ||
|
||||||
|
org.canAccessImportExport
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canAccessMembersTab(org: Organization): boolean {
|
export function canAccessMembersTab(org: Organization): boolean {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
bitTabListItem
|
bitTabListItem
|
||||||
[routerLink]="disabled ? null : route"
|
[routerLink]="disabled ? null : route"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
|
[routerLinkActiveOptions]="routerLinkMatchOptions"
|
||||||
#rla="routerLinkActive"
|
#rla="routerLinkActive"
|
||||||
[active]="rla.isActive"
|
[active]="rla.isActive"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FocusableOption } from "@angular/cdk/a11y";
|
import { FocusableOption } from "@angular/cdk/a11y";
|
||||||
import { AfterViewInit, Component, HostListener, Input, OnDestroy, ViewChild } from "@angular/core";
|
import { AfterViewInit, Component, HostListener, Input, OnDestroy, ViewChild } from "@angular/core";
|
||||||
import { RouterLinkActive } from "@angular/router";
|
import { IsActiveMatchOptions, RouterLinkActive } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { TabListItemDirective } from "../shared/tab-list-item.directive";
|
import { TabListItemDirective } from "../shared/tab-list-item.directive";
|
||||||
@@ -17,6 +17,13 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestr
|
|||||||
@ViewChild(TabListItemDirective) tabItem: TabListItemDirective;
|
@ViewChild(TabListItemDirective) tabItem: TabListItemDirective;
|
||||||
@ViewChild("rla") routerLinkActive: RouterLinkActive;
|
@ViewChild("rla") routerLinkActive: RouterLinkActive;
|
||||||
|
|
||||||
|
readonly routerLinkMatchOptions: IsActiveMatchOptions = {
|
||||||
|
queryParams: "ignored",
|
||||||
|
matrixParams: "ignored",
|
||||||
|
paths: "subset",
|
||||||
|
fragment: "ignored",
|
||||||
|
};
|
||||||
|
|
||||||
@Input() route: string;
|
@Input() route: string;
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user