diff --git a/frigate/api/app.py b/frigate/api/app.py index 4c0d3ead2..32843e81f 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -171,6 +171,18 @@ def config(request: Request): config["go2rtc"]["streams"][stream_name] = cleaned + # filter camera_groups by current user + username = request.headers.get("remote-user") + role = request.headers.get("remote-role") + + if username and role and role != "admin": + filtered_groups = {} + for group_name, group_config in config.get("camera_groups", {}).items(): + group_users = group_config.get("users") + if not group_users or username in group_users: + filtered_groups[group_name] = group_config + config["camera_groups"] = filtered_groups + config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} config["model"]["colormap"] = config_obj.model.colormap config["model"]["all_attributes"] = config_obj.model.all_attributes diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py index 65319001a..b69182806 100644 --- a/frigate/config/camera_group.py +++ b/frigate/config/camera_group.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union from pydantic import Field, field_validator @@ -23,6 +23,11 @@ class CameraGroupConfig(FrigateBaseModel): title="Sort order", description="Numeric order used to sort camera groups in the UI; larger numbers appear later.", ) + users: Optional[list[str]] = Field( + default=None, + title="Allowed users", + description="List of usernames who can see this group. If not set or empty, the group is visible to all users.", + ) @field_validator("cameras", mode="before") @classmethod diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 6d86c57f8..862b5e9cf 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -679,6 +679,11 @@ export function CameraGroupEdit({ const allowedCameras = useAllowedCameras(); const isAdmin = useIsAdmin(); + // fetch users list for the users multi-select (admin only) + const { data: usersList } = useSWR<{ username: string; role: string }[]>( + isAdmin ? "users" : null, + ); + const [openCamera, setOpenCamera] = useState(); const birdseyeConfig = useMemo(() => config?.birdseye, [config]); @@ -721,6 +726,7 @@ export function CameraGroupEdit({ .refine((value) => Object.keys(LuIcons).includes(value), { message: "Invalid icon", }), + users: z.array(z.string()).optional(), }); const onSubmit = useCallback( @@ -756,10 +762,18 @@ export function CameraGroupEdit({ const cameraQueries = values.cameras .map((cam) => `&camera_groups.${values.name}.cameras=${cam}`) .join(""); + const usersQueries = + values.users && values.users.length > 0 + ? values.users + .map( + (user) => `&camera_groups.${values.name}.users=${user}`, + ) + .join("") + : ""; axios .put( - `config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}`, + `config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}${usersQueries}`, { requires_restart: 0, }, @@ -828,6 +842,7 @@ export function CameraGroupEdit({ name: (editingGroup && editingGroup[0]) ?? "", icon: editingGroup && (editingGroup[1].icon as IconName), cameras: editingGroup && editingGroup[1].cameras, + users: (editingGroup && editingGroup[1].users) ?? [], }, }); @@ -970,6 +985,56 @@ export function CameraGroupEdit({ )} /> + {isAdmin && usersList && usersList.length > 0 && ( + <> + + ( + + {t("group.users.label", { defaultValue: "Assigned Users" })} + + {t("group.users.desc", { defaultValue: "If no users are selected, the group is visible to all users." })} + + +
+ {usersList.map((user) => ( + +
+ + { + const updatedUsers = checked + ? [...(field.value || []), user.username] + : (field.value || []).filter( + (u) => u !== user.username, + ); + form.setValue("users", updatedUsers); + }} + /> +
+
+ ))} +
+
+ )} + /> + + )} +
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index e3f794602..20ab93a58 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -312,6 +312,7 @@ export type CameraGroupConfig = { cameras: string[]; icon: IconName; order: number; + users?: string[]; }; export type StreamType = "no-streaming" | "smart" | "continuous";