nothingworry commited on
Commit
dc11950
·
1 Parent(s): 0122657

feat: implement role-based access control UI - hide/show features based on user role (viewer/editor/admin/owner)

Browse files
backend/tests/README_RETRY_TESTS.md CHANGED
@@ -262,3 +262,4 @@ For more information, see `TESTING_GUIDE.md` in the project root.
262
 
263
 
264
 
 
 
262
 
263
 
264
 
265
+
frontend/app/admin-rules/page.tsx CHANGED
@@ -7,6 +7,7 @@ import { AdminRulesPanel } from "@/components/admin-rules-panel";
7
  import { Footer } from "@/components/footer";
8
  import { useTenant } from "@/contexts/TenantContext";
9
  import { TenantSelector } from "@/components/tenant-selector";
 
10
 
11
  const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://localhost:8000";
12
 
@@ -51,6 +52,44 @@ export default function AdminRulesPage() {
51
  const [lastUpdated, setLastUpdated] = useState<string>("");
52
  const fileInputRef = useRef<HTMLInputElement>(null);
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  // Set initial time only on client side to avoid hydration mismatch
55
  useEffect(() => {
56
  setLastUpdated(new Date().toLocaleTimeString());
 
7
  import { Footer } from "@/components/footer";
8
  import { useTenant } from "@/contexts/TenantContext";
9
  import { TenantSelector } from "@/components/tenant-selector";
10
+ import { canManageRules } from "@/lib/permissions";
11
 
12
  const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? "http://localhost:8000";
13
 
 
52
  const [lastUpdated, setLastUpdated] = useState<string>("");
53
  const fileInputRef = useRef<HTMLInputElement>(null);
54
 
55
+ // Check permissions early
56
+ if (!canManageRules(role)) {
57
+ return (
58
+ <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
59
+ <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
60
+ <div className="flex items-center justify-between gap-3">
61
+ <div className="flex items-center gap-3 text-base font-semibold">
62
+ <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
63
+ IC
64
+ </span>
65
+ IntegraChat · Admin Rules
66
+ </div>
67
+ <div className="flex items-center gap-4">
68
+ <TenantSelector />
69
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
70
+ ← Back Home
71
+ </Link>
72
+ </div>
73
+ </div>
74
+ </header>
75
+
76
+ <div className="rounded-2xl border border-red-500/50 bg-red-500/10 p-8 text-center">
77
+ <h2 className="text-2xl font-bold text-red-300 mb-2">Access Denied</h2>
78
+ <p className="text-slate-300 mb-4">
79
+ You need <strong>Admin</strong> or <strong>Owner</strong> role to manage rules.
80
+ </p>
81
+ <p className="text-sm text-slate-400">
82
+ Your current role: <strong className="text-slate-200">{role.charAt(0).toUpperCase() + role.slice(1)}</strong>
83
+ </p>
84
+ <p className="text-sm text-slate-400 mt-2">
85
+ Please switch your role using the dropdown in the header.
86
+ </p>
87
+ </div>
88
+ <Footer />
89
+ </main>
90
+ );
91
+ }
92
+
93
  // Set initial time only on client side to avoid hydration mismatch
94
  useEffect(() => {
95
  setLastUpdated(new Date().toLocaleTimeString());
frontend/app/analytics/page.tsx CHANGED
@@ -1,10 +1,53 @@
 
 
1
  import Link from "next/link";
2
 
3
  import { AnalyticsPanel } from "@/components/analytics-panel";
4
  import { Footer } from "@/components/footer";
5
  import { TenantSelector } from "@/components/tenant-selector";
 
 
6
 
7
  export default function AnalyticsPage() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  return (
9
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
10
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
 
1
+ "use client";
2
+
3
  import Link from "next/link";
4
 
5
  import { AnalyticsPanel } from "@/components/analytics-panel";
6
  import { Footer } from "@/components/footer";
7
  import { TenantSelector } from "@/components/tenant-selector";
8
+ import { useTenant } from "@/contexts/TenantContext";
9
+ import { canViewAnalytics } from "@/lib/permissions";
10
 
11
  export default function AnalyticsPage() {
12
+ const { role } = useTenant();
13
+
14
+ if (!canViewAnalytics(role)) {
15
+ return (
16
+ <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
17
+ <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
18
+ <div className="flex items-center justify-between gap-3">
19
+ <div className="flex items-center gap-3 text-base font-semibold">
20
+ <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
21
+ IC
22
+ </span>
23
+ IntegraChat · Analytics
24
+ </div>
25
+ <div className="flex items-center gap-4">
26
+ <TenantSelector />
27
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
28
+ ← Back Home
29
+ </Link>
30
+ </div>
31
+ </div>
32
+ </header>
33
+
34
+ <div className="rounded-2xl border border-red-500/50 bg-red-500/10 p-8 text-center">
35
+ <h2 className="text-2xl font-bold text-red-300 mb-2">Access Denied</h2>
36
+ <p className="text-slate-300 mb-4">
37
+ You need <strong>Admin</strong> or <strong>Owner</strong> role to view analytics.
38
+ </p>
39
+ <p className="text-sm text-slate-400">
40
+ Your current role: <strong className="text-slate-200">{role.charAt(0).toUpperCase() + role.slice(1)}</strong>
41
+ </p>
42
+ <p className="text-sm text-slate-400 mt-2">
43
+ Please switch your role using the dropdown in the header.
44
+ </p>
45
+ </div>
46
+ <Footer />
47
+ </main>
48
+ );
49
+ }
50
+
51
  return (
52
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
53
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
frontend/app/ingestion/page.tsx CHANGED
@@ -1,10 +1,53 @@
 
 
1
  import Link from "next/link";
2
 
3
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
4
  import { Footer } from "@/components/footer";
5
  import { TenantSelector } from "@/components/tenant-selector";
 
 
6
 
7
  export default function IngestionPage() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  return (
9
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
10
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
 
1
+ "use client";
2
+
3
  import Link from "next/link";
4
 
5
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
6
  import { Footer } from "@/components/footer";
7
  import { TenantSelector } from "@/components/tenant-selector";
8
+ import { useTenant } from "@/contexts/TenantContext";
9
+ import { canIngestDocuments } from "@/lib/permissions";
10
 
11
  export default function IngestionPage() {
12
+ const { role } = useTenant();
13
+
14
+ if (!canIngestDocuments(role)) {
15
+ return (
16
+ <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
17
+ <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
18
+ <div className="flex items-center justify-between gap-3">
19
+ <div className="flex items-center gap-3 text-base font-semibold">
20
+ <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
21
+ IC
22
+ </span>
23
+ IntegraChat · Data Ingestion
24
+ </div>
25
+ <div className="flex items-center gap-4">
26
+ <TenantSelector />
27
+ <Link href="/" className="text-xs font-semibold uppercase tracking-[0.3em] text-cyan-300 hover:text-white">
28
+ ← Back Home
29
+ </Link>
30
+ </div>
31
+ </div>
32
+ </header>
33
+
34
+ <div className="rounded-2xl border border-red-500/50 bg-red-500/10 p-8 text-center">
35
+ <h2 className="text-2xl font-bold text-red-300 mb-2">Access Denied</h2>
36
+ <p className="text-slate-300 mb-4">
37
+ You need <strong>Editor</strong>, <strong>Admin</strong>, or <strong>Owner</strong> role to ingest documents.
38
+ </p>
39
+ <p className="text-sm text-slate-400">
40
+ Your current role: <strong className="text-slate-200">{role.charAt(0).toUpperCase() + role.slice(1)}</strong>
41
+ </p>
42
+ <p className="text-sm text-slate-400 mt-2">
43
+ Please switch your role using the dropdown in the header.
44
+ </p>
45
+ </div>
46
+ <Footer />
47
+ </main>
48
+ );
49
+ }
50
+
51
  return (
52
  <main className="mx-auto flex min-h-screen max-w-5xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
53
  <header className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
frontend/app/page.tsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import Link from "next/link";
2
 
3
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
@@ -8,41 +10,71 @@ import { Footer } from "@/components/footer";
8
  import { Hero } from "@/components/hero";
9
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
10
  import { TenantSelector } from "@/components/tenant-selector";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- const navItems = [
13
- { label: "Data Ingestion", href: "/ingestion" },
14
- { label: "Chat Bot", href: "/chat" },
15
- { label: "Analytics", href: "/analytics" },
16
- { label: "Admin Rule Ingestion", href: "/admin-rules" },
17
- ];
 
 
 
 
 
 
 
 
18
 
19
  export default function Home() {
 
 
20
  return (
21
  <main className="mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
22
  <header className="flex flex-col gap-6 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
23
  <div className="flex flex-wrap items-center justify-between gap-3 text-sm">
24
- <div className="flex items-center gap-3 text-base font-semibold">
25
- <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
26
- IC
27
- </span>
28
- IntegraChat Operator Console
29
- </div>
30
- <div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.4em] text-slate-400">
31
- FastAPI · MCP Servers · Celery · Next.js
32
- </div>
33
  </div>
34
  <div className="flex flex-wrap items-center justify-between gap-4">
35
- <nav className="flex flex-wrap gap-2">
36
- {navItems.map((item) => (
37
- <Link
38
- key={item.href}
39
- href={item.href}
40
- className="rounded-full border border-white/15 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-slate-200 transition hover:border-cyan-400 hover:text-white"
41
- >
42
- {item.label}
43
- </Link>
44
- ))}
45
- </nav>
46
  <TenantSelector />
47
  </div>
48
  </header>
@@ -50,21 +82,27 @@ export default function Home() {
50
  <Hero />
51
  <FeatureGrid />
52
 
53
- <section id="data-ingestion" className="scroll-mt-28">
54
- <KnowledgeBasePanel />
55
- </section>
 
 
56
 
57
  <section id="chat-bot" className="scroll-mt-28">
58
- <ChatPanel />
59
  </section>
60
 
61
- <section id="analytics" className="scroll-mt-28">
62
- <AnalyticsPanel />
63
- </section>
 
 
64
 
65
- <section id="admin-rules" className="scroll-mt-28">
66
- <AdminRulesPanel />
67
- </section>
 
 
68
 
69
  <Footer />
70
  </main>
 
1
+ "use client";
2
+
3
  import Link from "next/link";
4
 
5
  import { AdminRulesPanel } from "@/components/admin-rules-panel";
 
10
  import { Hero } from "@/components/hero";
11
  import { KnowledgeBasePanel } from "@/components/knowledge-base-panel";
12
  import { TenantSelector } from "@/components/tenant-selector";
13
+ import { useTenant } from "@/contexts/TenantContext";
14
+ import {
15
+ canManageRules,
16
+ canViewAnalytics,
17
+ canIngestDocuments,
18
+ } from "@/lib/permissions";
19
+
20
+ function Navigation() {
21
+ const { role } = useTenant();
22
+
23
+ const navItems = [
24
+ {
25
+ label: "Data Ingestion",
26
+ href: "/ingestion",
27
+ visible: canIngestDocuments(role),
28
+ },
29
+ { label: "Chat Bot", href: "/chat", visible: true }, // Chat is available to all
30
+ {
31
+ label: "Analytics",
32
+ href: "/analytics",
33
+ visible: canViewAnalytics(role),
34
+ },
35
+ {
36
+ label: "Admin Rule Ingestion",
37
+ href: "/admin-rules",
38
+ visible: canManageRules(role),
39
+ },
40
+ ];
41
+
42
+ const visibleNavItems = navItems.filter((item) => item.visible);
43
 
44
+ return (
45
+ <nav className="flex flex-wrap gap-2">
46
+ {visibleNavItems.map((item) => (
47
+ <Link
48
+ key={item.href}
49
+ href={item.href}
50
+ className="rounded-full border border-white/15 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-slate-200 transition hover:border-cyan-400 hover:text-white"
51
+ >
52
+ {item.label}
53
+ </Link>
54
+ ))}
55
+ </nav>
56
+ );
57
+ }
58
 
59
  export default function Home() {
60
+ const { role } = useTenant();
61
+
62
  return (
63
  <main className="mx-auto flex min-h-screen max-w-6xl flex-col gap-10 px-4 pb-16 pt-12 sm:px-6 lg:px-8">
64
  <header className="flex flex-col gap-6 rounded-2xl border border-white/10 bg-white/5 px-6 py-6 text-slate-100 shadow-lg shadow-slate-950/40">
65
  <div className="flex flex-wrap items-center justify-between gap-3 text-sm">
66
+ <div className="flex items-center gap-3 text-base font-semibold">
67
+ <span className="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-sky-400 to-cyan-500 text-slate-950">
68
+ IC
69
+ </span>
70
+ IntegraChat Operator Console
71
+ </div>
72
+ <div className="flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.4em] text-slate-400">
73
+ FastAPI · MCP Servers · Celery · Next.js
74
+ </div>
75
  </div>
76
  <div className="flex flex-wrap items-center justify-between gap-4">
77
+ <Navigation />
 
 
 
 
 
 
 
 
 
 
78
  <TenantSelector />
79
  </div>
80
  </header>
 
82
  <Hero />
83
  <FeatureGrid />
84
 
85
+ {canIngestDocuments(role) && (
86
+ <section id="data-ingestion" className="scroll-mt-28">
87
+ <KnowledgeBasePanel />
88
+ </section>
89
+ )}
90
 
91
  <section id="chat-bot" className="scroll-mt-28">
92
+ <ChatPanel />
93
  </section>
94
 
95
+ {canViewAnalytics(role) && (
96
+ <section id="analytics" className="scroll-mt-28">
97
+ <AnalyticsPanel />
98
+ </section>
99
+ )}
100
 
101
+ {canManageRules(role) && (
102
+ <section id="admin-rules" className="scroll-mt-28">
103
+ <AdminRulesPanel />
104
+ </section>
105
+ )}
106
 
107
  <Footer />
108
  </main>
frontend/components/analytics-panel.tsx CHANGED
@@ -40,6 +40,12 @@ export function AnalyticsPanel() {
40
  },
41
  });
42
  if (!res.ok) {
 
 
 
 
 
 
43
  throw new Error(`Analytics endpoint returned ${res.status}`);
44
  }
45
  const payload: AnalyticsOverview = await res.json();
 
40
  },
41
  });
42
  if (!res.ok) {
43
+ if (res.status === 403) {
44
+ const errorData = await res.json().catch(() => ({}));
45
+ throw new Error(
46
+ errorData.detail || "Access denied. You need Admin or Owner role to view analytics."
47
+ );
48
+ }
49
  throw new Error(`Analytics endpoint returned ${res.status}`);
50
  }
51
  const payload: AnalyticsOverview = await res.json();
frontend/components/knowledge-base-panel.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { useState, useRef, useEffect } from "react";
4
  import Link from "next/link";
5
  import { useTenant } from "@/contexts/TenantContext";
 
6
 
7
  type SearchResult = {
8
  text: string;
@@ -23,6 +24,7 @@ const API_BASE =
23
 
24
  export function KnowledgeBasePanel() {
25
  const { tenantId, isLoading: tenantLoading, role } = useTenant();
 
26
  const [searchQuery, setSearchQuery] = useState("");
27
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
28
  const [isSearching, setIsSearching] = useState(false);
@@ -537,7 +539,7 @@ export function KnowledgeBasePanel() {
537
  >
538
  {isLoadingDocs ? "Loading…" : "Refresh"}
539
  </button>
540
- {documents.length > 0 && (
541
  <button
542
  onClick={handleDeleteAll}
543
  disabled={isDeletingAll}
@@ -583,13 +585,15 @@ export function KnowledgeBasePanel() {
583
  : doc.text}
584
  </p>
585
  </div>
586
- <button
587
- onClick={() => handleDeleteDocument(doc.id)}
588
- disabled={isDeleting === doc.id}
589
- className="flex-shrink-0 rounded-lg border border-red-500/50 bg-red-500/10 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
590
- >
591
- {isDeleting === doc.id ? "Deleting…" : "Delete"}
592
- </button>
 
 
593
  </div>
594
  ))}
595
  </div>
 
3
  import { useState, useRef, useEffect } from "react";
4
  import Link from "next/link";
5
  import { useTenant } from "@/contexts/TenantContext";
6
+ import { canDeleteDocuments } from "@/lib/permissions";
7
 
8
  type SearchResult = {
9
  text: string;
 
24
 
25
  export function KnowledgeBasePanel() {
26
  const { tenantId, isLoading: tenantLoading, role } = useTenant();
27
+ const canDelete = canDeleteDocuments(role);
28
  const [searchQuery, setSearchQuery] = useState("");
29
  const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
30
  const [isSearching, setIsSearching] = useState(false);
 
539
  >
540
  {isLoadingDocs ? "Loading…" : "Refresh"}
541
  </button>
542
+ {canDelete && documents.length > 0 && (
543
  <button
544
  onClick={handleDeleteAll}
545
  disabled={isDeletingAll}
 
585
  : doc.text}
586
  </p>
587
  </div>
588
+ {canDelete && (
589
+ <button
590
+ onClick={() => handleDeleteDocument(doc.id)}
591
+ disabled={isDeleting === doc.id}
592
+ className="flex-shrink-0 rounded-lg border border-red-500/50 bg-red-500/10 px-3 py-1.5 text-xs font-semibold text-red-300 transition hover:bg-red-500/20 disabled:opacity-60"
593
+ >
594
+ {isDeleting === doc.id ? "Deleting…" : "Delete"}
595
+ </button>
596
+ )}
597
  </div>
598
  ))}
599
  </div>
frontend/lib/permissions.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Permission utilities for role-based access control
3
+ * Maps frontend roles to backend permission actions
4
+ */
5
+
6
+ export type UserRole = "viewer" | "editor" | "admin" | "owner";
7
+
8
+ /**
9
+ * Permission actions that match backend definitions
10
+ */
11
+ type PermissionAction =
12
+ | "manage_rules" // Admin/Owner only
13
+ | "ingest_documents" // Editor/Admin/Owner
14
+ | "delete_documents" // Admin/Owner only
15
+ | "view_analytics"; // Admin/Owner only
16
+
17
+ /**
18
+ * Permission matrix matching backend access_control.py
19
+ */
20
+ const PERMISSIONS: Record<PermissionAction, UserRole[]> = {
21
+ manage_rules: ["admin", "owner"],
22
+ ingest_documents: ["editor", "admin", "owner"],
23
+ delete_documents: ["admin", "owner"],
24
+ view_analytics: ["admin", "owner"],
25
+ };
26
+
27
+ /**
28
+ * Check if a role has permission for an action
29
+ */
30
+ export function hasPermission(role: UserRole, action: PermissionAction): boolean {
31
+ const allowedRoles = PERMISSIONS[action];
32
+ return allowedRoles.includes(role);
33
+ }
34
+
35
+ /**
36
+ * Check if user can manage rules (admin/owner only)
37
+ */
38
+ export function canManageRules(role: UserRole): boolean {
39
+ return hasPermission(role, "manage_rules");
40
+ }
41
+
42
+ /**
43
+ * Check if user can ingest documents (editor/admin/owner)
44
+ */
45
+ export function canIngestDocuments(role: UserRole): boolean {
46
+ return hasPermission(role, "ingest_documents");
47
+ }
48
+
49
+ /**
50
+ * Check if user can delete documents (admin/owner only)
51
+ */
52
+ export function canDeleteDocuments(role: UserRole): boolean {
53
+ return hasPermission(role, "delete_documents");
54
+ }
55
+
56
+ /**
57
+ * Check if user can view analytics (admin/owner only)
58
+ */
59
+ export function canViewAnalytics(role: UserRole): boolean {
60
+ return hasPermission(role, "view_analytics");
61
+ }
62
+
63
+ /**
64
+ * Check if user has admin-level access (admin or owner)
65
+ */
66
+ export function isAdminOrOwner(role: UserRole): boolean {
67
+ return role === "admin" || role === "owner";
68
+ }
69
+
70
+ /**
71
+ * Check if user has editor-level access or higher
72
+ */
73
+ export function isEditorOrAbove(role: UserRole): boolean {
74
+ return role === "editor" || role === "admin" || role === "owner";
75
+ }
76
+