Add validations to shared link

This commit is contained in:
0x464e 2026-03-19 21:09:58 +02:00
parent b4a632e818
commit 878e81e05b
No known key found for this signature in database
GPG Key ID: E6D221DF6CBFBFFA
2 changed files with 41 additions and 2 deletions

View File

@ -43,7 +43,9 @@
}, },
"documentTitle": "Review - Frigate", "documentTitle": "Review - Frigate",
"recordings": { "recordings": {
"documentTitle": "Recordings - Frigate" "documentTitle": "Recordings - Frigate",
"invalidSharedLink": "Unable to open timestamped recording link due to parsing error.",
"invalidSharedCamera": "Unable to open timestamped recording link due to an unknown or unauthorized camera."
}, },
"calendarFilter": { "calendarFilter": {
"last24Hours": "Last 24 Hours" "last24Hours": "Last 24 Hours"

View File

@ -30,9 +30,10 @@ import EventView from "@/views/events/EventView";
import MotionSearchView from "@/views/motion-search/MotionSearchView"; import MotionSearchView from "@/views/motion-search/MotionSearchView";
import { RecordingView } from "@/views/recording/RecordingView"; import { RecordingView } from "@/views/recording/RecordingView";
import axios from "axios"; import axios from "axios";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
export default function Events() { export default function Events() {
@ -74,6 +75,7 @@ export default function Events() {
const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>( const [motionSearchDay, setMotionSearchDay] = useState<Date | undefined>(
undefined, undefined,
); );
const handledReviewLinkRef = useRef<string | null>(null);
const motionSearchCameras = useMemo(() => { const motionSearchCameras = useMemo(() => {
if (!config?.cameras) { if (!config?.cameras) {
@ -257,16 +259,48 @@ export default function Events() {
const timezone = searchParams.get(RECORDING_REVIEW_TIMEZONE_PARAM); const timezone = searchParams.get(RECORDING_REVIEW_TIMEZONE_PARAM);
if (!timestamp) { if (!timestamp) {
handledReviewLinkRef.current = null;
return;
}
if (!config) {
return; return;
} }
const camera = location.hash const camera = location.hash
? decodeURIComponent(location.hash.substring(1)) ? decodeURIComponent(location.hash.substring(1))
: null; : null;
const reviewLinkKey = `${camera ?? ""}|${timestamp}|${timezone ?? ""}`;
if (handledReviewLinkRef.current === reviewLinkKey) {
return;
}
handledReviewLinkRef.current = reviewLinkKey;
const reviewLink = parseRecordingReviewLink(camera, timestamp, timezone); const reviewLink = parseRecordingReviewLink(camera, timestamp, timezone);
if (!reviewLink) { if (!reviewLink) {
toast.error(t("recordings.invalidSharedLink"), {
position: "top-center",
});
navigate(location.pathname, {
state: location.state,
replace: true,
});
return;
}
// reject unknown or unauthorized cameras before switching into
// recording view so bad links cleanly fall back to plain /review
const validCamera =
config.cameras[reviewLink.camera] &&
allowedCameras.includes(reviewLink.camera);
if (!validCamera) {
toast.error(t("recordings.invalidSharedCamera"), {
position: "top-center",
});
navigate(location.pathname, { navigate(location.pathname, {
state: location.state, state: location.state,
replace: true, replace: true,
@ -298,11 +332,14 @@ export default function Events() {
location.hash, location.hash,
location.pathname, location.pathname,
location.state, location.state,
config,
navigate, navigate,
allowedCameras,
getReviewDayBounds, getReviewDayBounds,
reviewFilter, reviewFilter,
searchParams, searchParams,
setReviewFilter, setReviewFilter,
t,
]); ]);
// review paging // review paging