fix camera group access for non admin users

changes from previous PR wrongly included users from the standard viewer role (but excluded custom viewer roles)
This commit is contained in:
Josh Hawkins 2025-11-26 10:42:22 -06:00
parent 5919b56ffb
commit 890ae6ae50
3 changed files with 19 additions and 19 deletions

View File

@ -78,7 +78,7 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { CameraNameLabel } from "../camera/FriendlyNameLabel"; import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useIsCustomRole } from "@/hooks/use-is-custom-role"; import { useIsAdmin } from "@/hooks/use-is-admin";
type CameraGroupSelectorProps = { type CameraGroupSelectorProps = {
className?: string; className?: string;
@ -88,7 +88,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
const { t } = useTranslation(["components/camera"]); const { t } = useTranslation(["components/camera"]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const allowedCameras = useAllowedCameras(); const allowedCameras = useAllowedCameras();
const isCustomRole = useIsCustomRole(); const isAdmin = useIsAdmin();
// tooltip // tooltip
@ -124,7 +124,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
const allGroups = Object.entries(config.camera_groups); const allGroups = Object.entries(config.camera_groups);
// If custom role, filter out groups where user has no accessible cameras // If custom role, filter out groups where user has no accessible cameras
if (isCustomRole) { if (!isAdmin) {
return allGroups return allGroups
.filter(([, groupConfig]) => { .filter(([, groupConfig]) => {
// Check if user has access to at least one camera in this group // Check if user has access to at least one camera in this group
@ -136,7 +136,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
} }
return allGroups.sort((a, b) => a[1].order - b[1].order); return allGroups.sort((a, b) => a[1].order - b[1].order);
}, [config, allowedCameras, isCustomRole]); }, [config, allowedCameras, isAdmin]);
// add group // add group
@ -153,7 +153,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
activeGroup={group} activeGroup={group}
setGroup={setGroup} setGroup={setGroup}
deleteGroup={deleteGroup} deleteGroup={deleteGroup}
isCustomRole={isCustomRole} isAdmin={isAdmin}
/> />
<Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}> <Scroller className={`${isMobile ? "whitespace-nowrap" : ""}`}>
<div <div
@ -221,7 +221,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
); );
})} })}
{!isCustomRole && ( {isAdmin && (
<Button <Button
className="bg-secondary text-muted-foreground" className="bg-secondary text-muted-foreground"
aria-label={t("group.add")} aria-label={t("group.add")}
@ -245,7 +245,7 @@ type NewGroupDialogProps = {
activeGroup?: string; activeGroup?: string;
setGroup: (value: string | undefined, replace?: boolean | undefined) => void; setGroup: (value: string | undefined, replace?: boolean | undefined) => void;
deleteGroup: () => void; deleteGroup: () => void;
isCustomRole?: boolean; isAdmin?: boolean;
}; };
function NewGroupDialog({ function NewGroupDialog({
open, open,
@ -254,7 +254,7 @@ function NewGroupDialog({
activeGroup, activeGroup,
setGroup, setGroup,
deleteGroup, deleteGroup,
isCustomRole, isAdmin,
}: NewGroupDialogProps) { }: NewGroupDialogProps) {
const { t } = useTranslation(["components/camera"]); const { t } = useTranslation(["components/camera"]);
const { mutate: updateConfig } = useSWR<FrigateConfig>("config"); const { mutate: updateConfig } = useSWR<FrigateConfig>("config");
@ -390,7 +390,7 @@ function NewGroupDialog({
> >
<Title>{t("group.label")}</Title> <Title>{t("group.label")}</Title>
<Description className="sr-only">{t("group.edit")}</Description> <Description className="sr-only">{t("group.edit")}</Description>
{!isCustomRole && ( {isAdmin && (
<div <div
className={cn( className={cn(
"absolute", "absolute",
@ -422,7 +422,7 @@ function NewGroupDialog({
group={group} group={group}
onDeleteGroup={() => onDeleteGroup(group[0])} onDeleteGroup={() => onDeleteGroup(group[0])}
onEditGroup={() => onEditGroup(group)} onEditGroup={() => onEditGroup(group)}
isReadOnly={isCustomRole} isReadOnly={!isAdmin}
/> />
))} ))}
</div> </div>
@ -677,7 +677,7 @@ export function CameraGroupEdit({
); );
const allowedCameras = useAllowedCameras(); const allowedCameras = useAllowedCameras();
const isCustomRole = useIsCustomRole(); const isAdmin = useIsAdmin();
const [openCamera, setOpenCamera] = useState<string | null>(); const [openCamera, setOpenCamera] = useState<string | null>();
@ -867,7 +867,7 @@ export function CameraGroupEdit({
<FormMessage /> <FormMessage />
{[ {[
...(birdseyeConfig?.enabled && ...(birdseyeConfig?.enabled &&
(!isCustomRole || "birdseye" in allowedCameras) (isAdmin || "birdseye" in allowedCameras)
? ["birdseye"] ? ["birdseye"]
: []), : []),
...Object.keys(config?.cameras ?? {}) ...Object.keys(config?.cameras ?? {})

View File

@ -14,12 +14,12 @@ import { useTranslation } from "react-i18next";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useIsCustomRole } from "@/hooks/use-is-custom-role"; import { useIsAdmin } from "@/hooks/use-is-admin";
function Live() { function Live() {
const { t } = useTranslation(["views/live"]); const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const isCustomRole = useIsCustomRole(); const isAdmin = useIsAdmin();
// selection // selection
@ -94,7 +94,7 @@ function Live() {
const includesBirdseye = useMemo(() => { const includesBirdseye = useMemo(() => {
// Restricted users should never have access to birdseye // Restricted users should never have access to birdseye
if (isCustomRole) { if (!isAdmin) {
return false; return false;
} }
@ -109,7 +109,7 @@ function Live() {
} else { } else {
return false; return false;
} }
}, [config, cameraGroup, isCustomRole]); }, [config, cameraGroup, isAdmin]);
const cameras = useMemo(() => { const cameras = useMemo(() => {
if (!config) { if (!config) {

View File

@ -54,7 +54,7 @@ import { useTranslation } from "react-i18next";
import { EmptyCard } from "@/components/card/EmptyCard"; import { EmptyCard } from "@/components/card/EmptyCard";
import { BsFillCameraVideoOffFill } from "react-icons/bs"; import { BsFillCameraVideoOffFill } from "react-icons/bs";
import { AuthContext } from "@/context/auth-context"; import { AuthContext } from "@/context/auth-context";
import { useIsCustomRole } from "@/hooks/use-is-custom-role"; import { useIsAdmin } from "@/hooks/use-is-admin";
type LiveDashboardViewProps = { type LiveDashboardViewProps = {
cameras: CameraConfig[]; cameras: CameraConfig[];
@ -661,10 +661,10 @@ export default function LiveDashboardView({
function NoCameraView() { function NoCameraView() {
const { t } = useTranslation(["views/live"]); const { t } = useTranslation(["views/live"]);
const { auth } = useContext(AuthContext); const { auth } = useContext(AuthContext);
const isCustomRole = useIsCustomRole(); const isAdmin = useIsAdmin();
// Check if this is a restricted user with no cameras in this group // Check if this is a restricted user with no cameras in this group
const isRestricted = isCustomRole && auth.isAuthenticated; const isRestricted = !isAdmin && auth.isAuthenticated;
return ( return (
<div className="flex size-full items-center justify-center"> <div className="flex size-full items-center justify-center">