mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-24 18:26:51 +03:00
Miscellaneous fixes (#24352)
* fix recordings unavailable endpoint when no params are provided
we already import the datetime class directly, so those attribute lookups raised AttributeError and the request returned 500
* reject JWTs whose role is no longer in the config
`/auth` trusted the role inside the JWT and re-signed it on refresh without checking the config, so a user whose restricted role was deleted kept a session carrying a role that isn't in `auth.roles`. The media, clip, recording, export and go2rtc checks treat a missing role the same as a role with no camera list, so that session could open every camera. `/auth` now returns 401 for a token whose role isn't configured, which sends the user back through login, and login already falls back to `viewer` for a role that's gone.
* delete the deleted camera group's layout, not the open one's
Deleting a camera group removed the layout of the group being viewed, because the dialog's delete was bound to `${activeGroup}-draggable-layout`. It also cleared the saved group, and both ran before the `config/set` request whether or not it succeeded, so deleting one group while viewing another lost the open group's layout and left the deleted group's layout behind. The deleted group's own layout is now removed after a successful save, and the saved group is only cleared when it was the open group.
* don't block API when querying PTZ info
camera_ptz_info is async but waited on the ONVIF controller's future with future.result(), blocking the API event loop for as long as a slow or unreachable ONVIF camera took to answer (including reconnect attempts), so every other async request stalled with it. Await the future with asyncio.wrap_future instead. The coroutine runs on the controller's own loop and thread, so this cannot deadlock.
* for custom exports, only allow admin users to add to existing cases
follows the existing convention where attaching an export to an existing case is admin-only on `POST /export/{camera}/...` and `POST /exports/batch`
* drop pending edits for a camera or profile that no longer exists
* match cached preview frames to their camera exactly
https://github.com/blakeblackshear/frigate/pull/22594 added a trailing `-` to the `preview_{camera}` prefix so `camera` stopped matching `camera2`'s frames, but camera names can contain `-`, so `front` still matched `front-door`'s. After a restart, `front`'s preview recorder deleted this hour's frames of a matching camera that sorted before it and added the timestamps of one that sorted after it, so ffmpeg was asked for files that don't exist and that hour's preview was lost. The offline fallback for `latest.jpg` could also return `front-door`'s frame for `front`, even to a user without access to `front-door`, and a short export could take its fallback thumbnail from the other camera. These now compare the full camera name taken from the file name.
This commit is contained in:
@@ -10,6 +10,7 @@ import { MdHome } from "react-icons/md";
|
||||
import { Button, buttonVariants } from "../ui/button";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
@@ -66,7 +67,8 @@ import { z } from "zod";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { toast } from "sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||
import { deleteUserNamespacedKey } from "@/hooks/use-user-persistence";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as LuIcons from "react-icons/lu";
|
||||
@@ -499,9 +501,8 @@ function NewGroupDialog({
|
||||
const [editState, setEditState] = useState<"none" | "add" | "edit">("none");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [, , , deleteGridLayout] = useUserPersistence(
|
||||
`${activeGroup}-draggable-layout`,
|
||||
);
|
||||
const { auth } = useContext(AuthContext);
|
||||
const username = auth?.user?.username;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -513,15 +514,14 @@ function NewGroupDialog({
|
||||
|
||||
const onDeleteGroup = useCallback(
|
||||
async (name: string) => {
|
||||
deleteGridLayout();
|
||||
deleteGroup();
|
||||
|
||||
await axios
|
||||
.put(`config/set?camera_groups.${name}`, { requires_restart: 0 })
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
deleteUserNamespacedKey(`${name}-draggable-layout`, username);
|
||||
if (activeGroup == name) {
|
||||
// deleting current group
|
||||
deleteGroup();
|
||||
setGroup("default");
|
||||
}
|
||||
updateConfig();
|
||||
@@ -557,15 +557,7 @@ function NewGroupDialog({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[
|
||||
updateConfig,
|
||||
activeGroup,
|
||||
setGroup,
|
||||
setOpen,
|
||||
deleteGroup,
|
||||
deleteGridLayout,
|
||||
t,
|
||||
],
|
||||
[updateConfig, activeGroup, setGroup, setOpen, deleteGroup, username, t],
|
||||
);
|
||||
|
||||
const onSave = () => {
|
||||
|
||||
@@ -540,6 +540,35 @@ export default function Settings() {
|
||||
const [editingProfile, setEditingProfile] = useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
|
||||
// drop pending edits for a camera or profile that no longer exists
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
const isStale = (camera: string, profile?: string | null) =>
|
||||
!config.cameras[camera] || (!!profile && !config.profiles?.[profile]);
|
||||
const prune = <T,>(
|
||||
prev: Record<string, T>,
|
||||
stale: (key: string, value: T) => boolean,
|
||||
) => {
|
||||
const next = Object.fromEntries(
|
||||
Object.entries(prev).filter(([key, value]) => !stale(key, value)),
|
||||
);
|
||||
return Object.keys(next).length === Object.keys(prev).length
|
||||
? prev
|
||||
: next;
|
||||
};
|
||||
setEditingProfile((prev) => prune(prev, isStale));
|
||||
setPendingDataBySection((prev) =>
|
||||
prune(prev, (key) => {
|
||||
const [camera, section] = key.split("::");
|
||||
return (
|
||||
section !== undefined &&
|
||||
isStale(camera, parseProfileFromSectionPath(section).profileName)
|
||||
);
|
||||
}),
|
||||
);
|
||||
}, [config]);
|
||||
|
||||
const [profilesUIEnabled, setProfilesUIEnabled] = useState(false);
|
||||
|
||||
const allProfileNames = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user