mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-06 07:07:39 +03:00
Merge pull request #74 from ibs0d/claude/personal-camera-groups-jJXtB
feat: add personal camera groups with per-user visibility
This commit is contained in:
commit
6852f5cf13
@ -171,6 +171,18 @@ def config(request: Request):
|
|||||||
|
|
||||||
config["go2rtc"]["streams"][stream_name] = cleaned
|
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["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()}
|
||||||
config["model"]["colormap"] = config_obj.model.colormap
|
config["model"]["colormap"] = config_obj.model.colormap
|
||||||
config["model"]["all_attributes"] = config_obj.model.all_attributes
|
config["model"]["all_attributes"] = config_obj.model.all_attributes
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
@ -23,6 +23,11 @@ class CameraGroupConfig(FrigateBaseModel):
|
|||||||
title="Sort order",
|
title="Sort order",
|
||||||
description="Numeric order used to sort camera groups in the UI; larger numbers appear later.",
|
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")
|
@field_validator("cameras", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -679,6 +679,11 @@ export function CameraGroupEdit({
|
|||||||
const allowedCameras = useAllowedCameras();
|
const allowedCameras = useAllowedCameras();
|
||||||
const isAdmin = useIsAdmin();
|
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<string | null>();
|
const [openCamera, setOpenCamera] = useState<string | null>();
|
||||||
|
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
@ -721,6 +726,7 @@ export function CameraGroupEdit({
|
|||||||
.refine((value) => Object.keys(LuIcons).includes(value), {
|
.refine((value) => Object.keys(LuIcons).includes(value), {
|
||||||
message: "Invalid icon",
|
message: "Invalid icon",
|
||||||
}),
|
}),
|
||||||
|
users: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
@ -756,10 +762,18 @@ export function CameraGroupEdit({
|
|||||||
const cameraQueries = values.cameras
|
const cameraQueries = values.cameras
|
||||||
.map((cam) => `&camera_groups.${values.name}.cameras=${cam}`)
|
.map((cam) => `&camera_groups.${values.name}.cameras=${cam}`)
|
||||||
.join("");
|
.join("");
|
||||||
|
const usersQueries =
|
||||||
|
values.users && values.users.length > 0
|
||||||
|
? values.users
|
||||||
|
.map(
|
||||||
|
(user) => `&camera_groups.${values.name}.users=${user}`,
|
||||||
|
)
|
||||||
|
.join("")
|
||||||
|
: "";
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.put(
|
.put(
|
||||||
`config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}`,
|
`config/set?${renamingQuery}${orderQuery}&${iconQuery}${cameraQueries}${usersQueries}`,
|
||||||
{
|
{
|
||||||
requires_restart: 0,
|
requires_restart: 0,
|
||||||
},
|
},
|
||||||
@ -828,6 +842,7 @@ export function CameraGroupEdit({
|
|||||||
name: (editingGroup && editingGroup[0]) ?? "",
|
name: (editingGroup && editingGroup[0]) ?? "",
|
||||||
icon: editingGroup && (editingGroup[1].icon as IconName),
|
icon: editingGroup && (editingGroup[1].icon as IconName),
|
||||||
cameras: editingGroup && editingGroup[1].cameras,
|
cameras: editingGroup && editingGroup[1].cameras,
|
||||||
|
users: (editingGroup && editingGroup[1].users) ?? [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -970,6 +985,56 @@ export function CameraGroupEdit({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isAdmin && usersList && usersList.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="users"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("group.users.label", { defaultValue: "Assigned Users" })}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("group.users.desc", { defaultValue: "If no users are selected, the group is visible to all users." })}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
<div className="scrollbar-container max-h-[30dvh] overflow-y-auto">
|
||||||
|
{usersList.map((user) => (
|
||||||
|
<FormControl key={user.username}>
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<label
|
||||||
|
className="mx-2 w-full cursor-pointer text-primary"
|
||||||
|
htmlFor={`user-${user.username}`}
|
||||||
|
>
|
||||||
|
{user.username}
|
||||||
|
<span className="ml-2 text-xs text-muted-foreground">
|
||||||
|
({user.role})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id={`user-${user.username}`}
|
||||||
|
checked={
|
||||||
|
field.value?.includes(user.username) ?? false
|
||||||
|
}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const updatedUsers = checked
|
||||||
|
? [...(field.value || []), user.username]
|
||||||
|
: (field.value || []).filter(
|
||||||
|
(u) => u !== user.username,
|
||||||
|
);
|
||||||
|
form.setValue("users", updatedUsers);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Separator className="my-2 flex bg-secondary" />
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 py-5 md:pb-0">
|
<div className="flex flex-row gap-2 py-5 md:pb-0">
|
||||||
|
|||||||
@ -312,6 +312,7 @@ export type CameraGroupConfig = {
|
|||||||
cameras: string[];
|
cameras: string[];
|
||||||
icon: IconName;
|
icon: IconName;
|
||||||
order: number;
|
order: number;
|
||||||
|
users?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StreamType = "no-streaming" | "smart" | "continuous";
|
export type StreamType = "no-streaming" | "smart" | "continuous";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user