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,14 +1,17 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api/client";
|
||||
import type { AuditEvent } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
|
||||
export function Audit() {
|
||||
const { user } = useAuth();
|
||||
const [events, setEvents] = useState<AuditEvent[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [action, setAction] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== "operations_manager") return;
|
||||
setEvents(null);
|
||||
setError(null);
|
||||
const params = new URLSearchParams();
|
||||
@@ -17,7 +20,16 @@ export function Audit() {
|
||||
.get<AuditEvent[]>(`/api/v1/audit?${params.toString()}`)
|
||||
.then(setEvents)
|
||||
.catch(() => setError("Audit trail is unavailable right now."));
|
||||
}, [action]);
|
||||
}, [action, user]);
|
||||
|
||||
if (user?.role !== "operations_manager") {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader eyebrow="Assurance / Immutable history" title="Audit trail" description="The audit trail is visible to Operations Managers only." />
|
||||
<p>Audit history is visible to Operations Managers only.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { Dashboard as DashboardData, KnowledgeHealth } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
import { Icon } from "../components/Icons";
|
||||
import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
|
||||
@@ -24,6 +25,9 @@ function localTime(value: string, withDate = false) {
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
const canSeeQuality = user?.role === "operations_manager";
|
||||
const canSeeAutomation = user?.role === "operations_manager";
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [knowledge, setKnowledge] = useState<KnowledgeHealth | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -68,7 +72,7 @@ export function Dashboard() {
|
||||
|
||||
<div className="operations-grid">
|
||||
<section className="work-panel attention-panel" aria-labelledby="attention-heading">
|
||||
<SectionHeading title="Attention queue" description={`${data.metrics.open_quality_issues} open quality issues · ${data.metrics.pending_or_failed_workflows} workflow exceptions`} action={<Link to="/data-quality">Review queue <Icon name="chevron" /></Link>} />
|
||||
<SectionHeading title="Attention queue" description={`${data.metrics.open_quality_issues} open quality issues · ${data.metrics.pending_or_failed_workflows} workflow exceptions`} action={canSeeQuality ? <Link to="/data-quality">Review queue <Icon name="chevron" /></Link> : undefined} />
|
||||
<div className="queue-controls">
|
||||
<label className="compact-search"><Icon name="search" /><span className="visually-hidden">Search attention queue</span><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Filter issues…" /></label>
|
||||
<label><span className="visually-hidden">Severity</span><select value={severity} onChange={(event) => setSeverity(event.target.value)}><option value="all">All severity</option><option value="high">Critical</option><option value="medium">Warning</option><option value="low">Info</option></select></label>
|
||||
@@ -80,7 +84,13 @@ export function Dashboard() {
|
||||
<SeverityBadge severity={item.severity} />
|
||||
<div className="queue-copy">
|
||||
<p className="attention-title">
|
||||
{item.issue_ref ? <Link to={`/data-quality/${item.issue_ref}`}>{item.title}</Link> : item.link_type === "vehicle" ? <Link to={`/vehicles/${item.link_ref}`}>{item.title}</Link> : item.title}
|
||||
{item.issue_ref && canSeeQuality ? (
|
||||
<Link to={`/data-quality/${item.issue_ref}`}>{item.title}</Link>
|
||||
) : item.link_type === "vehicle" ? (
|
||||
<Link to={`/vehicles/${item.link_ref}`}>{item.title}</Link>
|
||||
) : (
|
||||
item.title
|
||||
)}
|
||||
</p>
|
||||
<p className="attention-detail">{item.detail}</p>
|
||||
</div>
|
||||
@@ -110,7 +120,7 @@ export function Dashboard() {
|
||||
|
||||
<div className="secondary-grid">
|
||||
<section className="work-panel integration-panel" aria-labelledby="integration-heading">
|
||||
<SectionHeading title="Integration pulse" description="Current evidence from connected services" action={<Link to="/automation">System detail <Icon name="chevron" /></Link>} />
|
||||
<SectionHeading title="Integration pulse" description="Current evidence from connected services" action={canSeeAutomation ? <Link to="/automation">System detail <Icon name="chevron" /></Link> : undefined} />
|
||||
<ul className="integration-list">
|
||||
<li><IntegrationMark kind="n8n" /><div><strong>n8n delivery</strong><span>{latestRun ? `Latest event ${latestRun.aggregate_ref}` : "No workflow evidence recorded"}</span></div><StatusBadge status={n8nState.toLowerCase().replace(/ /g, "_")} /></li>
|
||||
<li><IntegrationMark kind="rag" /><div><strong>RAGcore knowledge</strong><span>{knowledge ? `${knowledge.document_count} procedures indexed` : "Health check unavailable"}</span></div><StatusBadge status={knowledge?.available ? "available" : "unavailable"} /></li>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { DataQualityIssue } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { SeverityBadge, StatusBadge } from "../components/Badge";
|
||||
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
|
||||
@@ -14,12 +15,14 @@ const RULE_TYPES = [
|
||||
];
|
||||
|
||||
export function DataQuality() {
|
||||
const { user } = useAuth();
|
||||
const [issues, setIssues] = useState<DataQualityIssue[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState("open");
|
||||
const [ruleType, setRuleType] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== "operations_manager") return;
|
||||
setIssues(null);
|
||||
setError(null);
|
||||
const params = new URLSearchParams();
|
||||
@@ -29,7 +32,16 @@ export function DataQuality() {
|
||||
.get<DataQualityIssue[]>(`/api/v1/data-quality/issues?${params.toString()}`)
|
||||
.then(setIssues)
|
||||
.catch(() => setError("Data-quality issues are unavailable right now."));
|
||||
}, [status, ruleType]);
|
||||
}, [status, ruleType, user]);
|
||||
|
||||
if (user?.role !== "operations_manager") {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader eyebrow="Assurance / Workbench" title="Data quality" description="The quality workbench is visible to Operations Managers only." />
|
||||
<p>Data-quality evidence and resolutions are visible to Operations Managers only.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
|
||||
@@ -167,6 +167,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
}
|
||||
|
||||
export function DataQualityIssueDetail() {
|
||||
const { user } = useAuth();
|
||||
const { publicRef } = useParams<{ publicRef: string }>();
|
||||
const [issue, setIssue] = useState<IssueDetail | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -181,10 +182,11 @@ export function DataQualityIssueDetail() {
|
||||
}, [publicRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== "operations_manager") return;
|
||||
setIssue(null);
|
||||
setError(null);
|
||||
load();
|
||||
}, [load]);
|
||||
}, [load, user]);
|
||||
|
||||
async function handleAction(action: "defer" | "reject") {
|
||||
if (!issue) return;
|
||||
@@ -197,6 +199,15 @@ export function DataQualityIssueDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.role !== "operations_manager") {
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader eyebrow="Assurance / Workbench" title="Data quality issue" description="The quality workbench is visible to Operations Managers only." />
|
||||
<p>Data-quality evidence and resolutions are visible to Operations Managers only.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) return <ErrorState message={error} />;
|
||||
if (!issue) return <LoadingState label="Loading issue evidence…" />;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user