mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-29 15:19:00 +03:00
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:
@@ -20,24 +20,23 @@ import {
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
|
||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const { login } = React.useContext(AuthContext);
|
||||
|
||||
const formSchema = z.object({
|
||||
user: z.string(),
|
||||
password: z.string(),
|
||||
user: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
user: "",
|
||||
password: "",
|
||||
},
|
||||
defaultValues: { user: "", password: "" },
|
||||
});
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
@@ -50,11 +49,14 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
password: values.password,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": 1,
|
||||
},
|
||||
headers: { "X-CSRF-TOKEN": 1 },
|
||||
},
|
||||
);
|
||||
const profileRes = await axios.get("/profile", { withCredentials: true });
|
||||
login({
|
||||
username: profileRes.data.username,
|
||||
role: profileRes.data.role || "viewer",
|
||||
});
|
||||
window.location.href = baseUrl;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useContext } from "react";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
|
||||
export default function ProtectedRoute({
|
||||
requiredRoles,
|
||||
}: {
|
||||
requiredRoles: ("admin" | "viewer")[];
|
||||
}) {
|
||||
const { auth } = useContext(AuthContext);
|
||||
|
||||
if (auth.isLoading) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
);
|
||||
}
|
||||
|
||||
// Unauthenticated mode
|
||||
if (!auth.isAuthenticated) {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
// Authenticated mode (8971): require login
|
||||
if (!auth.user) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback
|
||||
// though isAuthenticated should catch this
|
||||
if (auth.user.role === null) {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
if (!requiredRoles.includes(auth.user.role)) {
|
||||
return <Navigate to="/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
Reference in New Issue
Block a user