UI viewer role (#16978)

* db migration

* db model

* assign admin role on password reset

* add role to jwt and api responses

* don't restrict api access for admins yet

* use json response

* frontend auth context

* update auth form for profile endpoint

* add access denied page

* add protected routes

* auth hook

* dialogs

* user settings view

* restrict viewer access to settings

* restrict camera functions for viewer role

* add password dialog to account menu

* spacing tweak

* migrator default to admin

* escape quotes in migrator

* ui tweaks

* tweaks

* colors

* colors

* fix merge conflict

* fix icons

* add api layer enforcement

* ui tweaks

* fix error message

* debug

* clean up

* remove print

* guard apis for admin only

* fix tests

* fix review tests

* use correct error responses from api in toasts

* add role to account menu
This commit is contained in:
Josh Hawkins
2025-03-08 10:01:08 -06:00
committed by GitHub
parent 6f9d9cd5a8
commit 74ca009b0b
50 changed files with 1951 additions and 732 deletions
+74
View File
@@ -0,0 +1,74 @@
import axios from "axios";
import { createContext, useEffect, useState } from "react";
import useSWR from "swr";
interface AuthState {
user: { username: string; role: "admin" | "viewer" | null } | null;
isLoading: boolean;
isAuthenticated: boolean; // true if auth is required
}
interface AuthContextType {
auth: AuthState;
login: (user: AuthState["user"]) => void;
logout: () => void;
}
export const AuthContext = createContext<AuthContextType>({
auth: { user: null, isLoading: true, isAuthenticated: false },
login: () => {},
logout: () => {},
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [auth, setAuth] = useState<AuthState>({
user: null,
isLoading: true,
isAuthenticated: false,
});
const { data: profile, error } = useSWR("/profile", {
revalidateOnFocus: false,
revalidateOnReconnect: true,
fetcher: (url) =>
axios.get(url, { withCredentials: true }).then((res) => res.data),
});
useEffect(() => {
if (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// auth required but not logged in
setAuth({ user: null, isLoading: false, isAuthenticated: true });
}
return;
}
if (profile) {
if (profile.username && profile.username !== "anonymous") {
const newUser = {
username: profile.username,
role: profile.role || "viewer",
};
setAuth({ user: newUser, isLoading: false, isAuthenticated: true });
} else {
// Unauthenticated mode (anonymous)
setAuth({ user: null, isLoading: false, isAuthenticated: false });
}
}
}, [profile, error]);
const login = (user: AuthState["user"]) => {
setAuth({ user, isLoading: false, isAuthenticated: true });
};
const logout = () => {
setAuth({ user: null, isLoading: false, isAuthenticated: true });
axios.get("/logout", { withCredentials: true });
};
return (
<AuthContext.Provider value={{ auth, login, logout }}>
{children}
</AuthContext.Provider>
);
}
+16 -13
View File
@@ -6,6 +6,7 @@ import { IconContext } from "react-icons";
import { TooltipProvider } from "@/components/ui/tooltip";
import { StatusBarMessagesProvider } from "@/context/statusbar-provider";
import { StreamingSettingsProvider } from "./streaming-settings-provider";
import { AuthProvider } from "./auth-context";
type TProvidersProps = {
children: ReactNode;
@@ -14,19 +15,21 @@ type TProvidersProps = {
function providers({ children }: TProvidersProps) {
return (
<RecoilRoot>
<ApiProvider>
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
<TooltipProvider>
<IconContext.Provider value={{ size: "20" }}>
<StatusBarMessagesProvider>
<StreamingSettingsProvider>
{children}
</StreamingSettingsProvider>
</StatusBarMessagesProvider>
</IconContext.Provider>
</TooltipProvider>
</ThemeProvider>
</ApiProvider>
<AuthProvider>
<ApiProvider>
<ThemeProvider defaultTheme="system" storageKey="frigate-ui-theme">
<TooltipProvider>
<IconContext.Provider value={{ size: "20" }}>
<StatusBarMessagesProvider>
<StreamingSettingsProvider>
{children}
</StreamingSettingsProvider>
</StatusBarMessagesProvider>
</IconContext.Provider>
</TooltipProvider>
</ThemeProvider>
</ApiProvider>
</AuthProvider>
</RecoilRoot>
);
}