frigate/web/src/App.tsx
Josh Hawkins ed1e3a7c9a
Enhance user roles to limit camera access (#20024)
* update config for roles and add validator

* ensure admin and viewer are never overridden

* add class method to user to retrieve all allowed cameras

* enforce config roles in auth api endpoints

* add camera access api dependency functions

* protect review endpoints

* protect preview endpoints

* rename param name for better fastapi injection matching

* remove unneeded

* protect export endpoints

* protect event endpoints

* protect media endpoints

* update auth hook for allowed cameras

* update default app view

* ensure anonymous user always returns all cameras

* limit cameras in explore

* cameras is already a list

* limit cameras in review/history

* limit cameras in live view

* limit cameras in camera groups

* only show face library and classification in sidebar for admin

* remove check in delete reviews

since admin role is required, no need to check camera access. fixes failing test

* pass request with camera access for tests

* more async

* camera access tests

* fix proxy auth tests

* allowed cameras for review tests

* combine event tests and refactor for camera access

* fix post validation for roles

* don't limit roles in create user dialog

* fix triggers endpoints

no need to run require camera access dep since the required role is admin

* fix type

* create and edit role dialogs

* delete role dialog

* fix role change dialog

* update settings view for roles

* i18n changes

* minor spacing tweaks

* docs

* use badges and camera name label component

* clarify docs

* display all cameras badge for admin and viewer

* i18n fix

* use validator to prevent reserved and empty roles from being assigned

* split users and roles into separate tabs in settings

* tweak docs

* clarify docs

* change icon

* don't memoize roles

always recalculate on component render
2025-09-12 05:19:29 -06:00

119 lines
4.0 KiB
TypeScript

import Providers from "@/context/providers";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Wrapper from "@/components/Wrapper";
import Sidebar from "@/components/navigation/Sidebar";
import { isDesktop, isMobile } from "react-device-detect";
import Statusbar from "./components/Statusbar";
import Bottombar from "./components/navigation/Bottombar";
import { Suspense, lazy } from "react";
import { Redirect } from "./components/navigation/Redirect";
import { cn } from "./lib/utils";
import { isPWA } from "./utils/isPWA";
import ProtectedRoute from "@/components/auth/ProtectedRoute";
import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig";
const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
const Explore = lazy(() => import("@/pages/Explore"));
const Exports = lazy(() => import("@/pages/Exports"));
const ConfigEditor = lazy(() => import("@/pages/ConfigEditor"));
const System = lazy(() => import("@/pages/System"));
const Settings = lazy(() => import("@/pages/Settings"));
const UIPlayground = lazy(() => import("@/pages/UIPlayground"));
const FaceLibrary = lazy(() => import("@/pages/FaceLibrary"));
const Classification = lazy(() => import("@/pages/ClassificationModel"));
const Logs = lazy(() => import("@/pages/Logs"));
const AccessDenied = lazy(() => import("@/pages/AccessDenied"));
function App() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
return (
<Providers>
<AuthProvider>
<BrowserRouter basename={window.baseUrl}>
<Wrapper>
{config?.safe_mode ? <SafeAppView /> : <DefaultAppView />}
</Wrapper>
</BrowserRouter>
</AuthProvider>
</Providers>
);
}
function DefaultAppView() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
return (
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
{isDesktop && <Statusbar />}
{isMobile && <Bottombar />}
<div
id="pageRoot"
className={cn(
"absolute right-0 top-0 overflow-hidden",
isMobile
? `bottom-${isPWA ? 16 : 12} left-0 md:bottom-16 landscape:bottom-14 landscape:md:bottom-16`
: "bottom-8 left-[52px]",
)}
>
<Suspense>
<Routes>
<Route
element={
<ProtectedRoute
requiredRoles={
config?.auth.roles
? Object.keys(config.auth.roles)
: ["admin", "viewer"]
}
/>
}
>
<Route index element={<Live />} />
<Route path="/review" element={<Events />} />
<Route path="/explore" element={<Explore />} />
<Route path="/export" element={<Exports />} />
<Route path="/settings" element={<Settings />} />
</Route>
<Route element={<ProtectedRoute requiredRoles={["admin"]} />}>
<Route path="/system" element={<System />} />
<Route path="/config" element={<ConfigEditor />} />
<Route path="/logs" element={<Logs />} />
<Route path="/faces" element={<FaceLibrary />} />
<Route path="/classification" element={<Classification />} />
<Route path="/playground" element={<UIPlayground />} />
</Route>
<Route path="/unauthorized" element={<AccessDenied />} />
<Route path="*" element={<Redirect to="/" />} />
</Routes>
</Suspense>
</div>
</div>
);
}
function SafeAppView() {
return (
<div className="size-full overflow-hidden">
<div
id="pageRoot"
className={cn("absolute bottom-0 left-0 right-0 top-0 overflow-hidden")}
>
<Suspense>
<ConfigEditor />
</Suspense>
</div>
</div>
);
}
export default App;