fix(auth): enforce role boundaries on data quality and audit
The data-quality workbench (list, detail, defer, reject) and the audit trail had no role gate at all beyond authentication -- confirmed live, a Rental Employee session could list and resolve data-quality issues and read the full audit trail through both the API and the UI, with only merge-customers and scan already restricted. Per the role matrix, both areas are Operations-Manager-only. Gate the remaining data-quality and audit endpoints with require_operations_manager, hide their nav items for Rental Employee, show the same restricted-message pattern Automation.tsx already used for direct URL access, and stop the dashboard from linking into now-restricted areas for that role.
This commit is contained in:
@@ -1,30 +1,37 @@
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import type { Role } from "../api/types";
|
||||
import { BrandMark, Icon, type IconName } from "./Icons";
|
||||
|
||||
const NAV_GROUPS: Array<{ label: string; items: Array<{ to: string; label: string; shortLabel: string; icon: IconName }> }> = [
|
||||
interface NavItem {
|
||||
to: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
icon: IconName;
|
||||
roles?: Role[];
|
||||
}
|
||||
|
||||
const NAV_GROUPS: Array<{ label: string; items: NavItem[] }> = [
|
||||
{
|
||||
label: "Operate",
|
||||
items: [
|
||||
{ to: "/dashboard", label: "Overview", shortLabel: "Overview", icon: "activity" },
|
||||
{ to: "/vehicles", label: "Fleet", shortLabel: "Fleet", icon: "fleet" },
|
||||
{ to: "/bookings", label: "Bookings", shortLabel: "Bookings", icon: "bookings" },
|
||||
{ to: "/data-quality", label: "Data quality", shortLabel: "Quality", icon: "quality" },
|
||||
{ to: "/data-quality", label: "Data quality", shortLabel: "Quality", icon: "quality", roles: ["operations_manager"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Assure",
|
||||
items: [
|
||||
{ to: "/knowledge", label: "Knowledge", shortLabel: "Knowledge", icon: "knowledge" },
|
||||
{ to: "/automation", label: "Integrations", shortLabel: "Systems", icon: "integrations" },
|
||||
{ to: "/audit", label: "Audit trail", shortLabel: "Audit", icon: "audit" },
|
||||
{ to: "/automation", label: "Integrations", shortLabel: "Systems", icon: "integrations", roles: ["operations_manager"] },
|
||||
{ to: "/audit", label: "Audit trail", shortLabel: "Audit", icon: "audit", roles: ["operations_manager"] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_ITEMS = NAV_GROUPS.flatMap((group) => group.items).slice(0, 5);
|
||||
|
||||
const SEARCH_DESTINATIONS = [
|
||||
{ to: "/dashboard", terms: ["overview", "dashboard", "readiness"] },
|
||||
{ to: "/vehicles", terms: ["fleet", "vehicle", "vehicles"] },
|
||||
@@ -43,6 +50,16 @@ export function Layout() {
|
||||
const [searchStatus, setSearchStatus] = useState("");
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const navGroups = useMemo(
|
||||
() =>
|
||||
NAV_GROUPS.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => !item.roles || (user && item.roles.includes(user.role))),
|
||||
})).filter((group) => group.items.length > 0),
|
||||
[user],
|
||||
);
|
||||
const mobileItems = useMemo(() => navGroups.flatMap((group) => group.items).slice(0, 5), [navGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
function focusGlobalSearch(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
||||
@@ -55,8 +72,8 @@ export function Layout() {
|
||||
return () => window.removeEventListener("keydown", focusGlobalSearch);
|
||||
}, []);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
async function handleLogout() {
|
||||
await logout();
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
@@ -99,7 +116,7 @@ export function Layout() {
|
||||
<div><strong>MobilityOps</strong><span>Control centre</span></div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation">
|
||||
{NAV_GROUPS.map((group) => (
|
||||
{navGroups.map((group) => (
|
||||
<div className="nav-group" key={group.label}>
|
||||
<p>{group.label}</p>
|
||||
<ul>
|
||||
@@ -166,7 +183,7 @@ export function Layout() {
|
||||
</div>
|
||||
|
||||
<nav className="mobile-nav" aria-label="Mobile navigation">
|
||||
{MOBILE_ITEMS.map((item) => (
|
||||
{mobileItems.map((item) => (
|
||||
<NavLink key={item.to} to={item.to}>
|
||||
<Icon name={item.icon} />
|
||||
<span>{item.shortLabel}</span>
|
||||
|
||||
Reference in New Issue
Block a user