mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-10 15:43:01 +03:00
Add on-demand Frigate+ API key validation (#24918)
* Add on-demand Frigate+ API key validation The Frigate+ settings page reported the API key as validated whenever it matched the expected format, without ever checking it with the server. - Clarify the status text to say the key matches the expected format - Add PlusApi.validate_key(), which requests a token from Frigate+ and reports the key as valid, invalid, unreachable, or not set; server errors and timeouts are not reported as an invalid key - Add an admin-only POST /plus/validate endpoint and regenerate the API spec - Add a "Validate with Frigate+" button that shows the result inline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde * Validate Frigate+ key asynchronously and keep cached token on bad replies - Use aiohttp for the validation request so the endpoint awaits it instead of blocking a worker thread - Only replace the cached token when the reply has a string accessToken and numeric expires; otherwise report unreachable so an unexpected response can't break later Frigate+ requests - Test against a local aiohttp server, covering timeouts, connection failures, non-JSON replies and unusable token shapes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde * Honor proxy settings when validating the Frigate+ key aiohttp ignores HTTP(S)_PROXY unless trust_env is set, so validation reported Frigate+ as unreachable on installs that need a proxy while other Frigate+ requests still worked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
52d469ed20
commit
81cf7be89a
@@ -1285,8 +1285,13 @@
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "Frigate+ API Key",
|
||||
"validated": "Frigate+ API key is detected and validated",
|
||||
"notValidated": "Frigate+ API key is not detected or not validated",
|
||||
"formatValid": "Frigate+ API key is detected and matches the expected format",
|
||||
"formatInvalid": "Frigate+ API key is not detected or does not match the expected format",
|
||||
"validate": "Validate with Frigate+",
|
||||
"validating": "Validating…",
|
||||
"serverValid": "Frigate+ accepted this API key",
|
||||
"serverInvalid": "Frigate+ rejected this API key",
|
||||
"serverUnreachable": "Unable to reach Frigate+ to validate this API key",
|
||||
"desc": "The Frigate+ API key enables integration with the Frigate+ service.",
|
||||
"plusLink": "Read more about Frigate+"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
import { CheckCircle2, XCircle } from "lucide-react";
|
||||
import axios from "axios";
|
||||
import { AlertTriangle, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { LuExternalLink } from "react-icons/lu";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -18,12 +19,34 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { isReplayCamera } from "@/utils/cameraUtil";
|
||||
import type { SettingsPageProps } from "@/views/settings/SingleSectionPage";
|
||||
import { getPrimaryModel } from "@/utils/modelUtil";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
|
||||
type PlusKeyStatus = "valid" | "invalid" | "unreachable" | "not_set";
|
||||
|
||||
type PlusValidateResponse = { success: boolean; status: PlusKeyStatus };
|
||||
|
||||
export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [keyStatus, setKeyStatus] = useState<PlusKeyStatus>();
|
||||
|
||||
const validateKey = useCallback(async () => {
|
||||
setIsValidating(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post<PlusValidateResponse>("plus/validate");
|
||||
setKeyStatus(response.data.status);
|
||||
} catch {
|
||||
setKeyStatus("unreachable");
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("documentTitle.frigatePlus");
|
||||
@@ -68,17 +91,55 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
|
||||
</>
|
||||
}
|
||||
content={
|
||||
<div className="flex items-center gap-2">
|
||||
{config?.plus?.enabled ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{config?.plus?.enabled ? (
|
||||
<CheckCircle2 className="size-5 shrink-0 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="size-5 shrink-0 text-red-500" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{config?.plus?.enabled
|
||||
? t("frigatePlus.apiKey.formatValid")
|
||||
: t("frigatePlus.apiKey.formatInvalid")}
|
||||
</span>
|
||||
</div>
|
||||
{config?.plus?.enabled && isAdmin && (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isValidating}
|
||||
onClick={validateKey}
|
||||
aria-label={t("frigatePlus.apiKey.validate")}
|
||||
>
|
||||
{isValidating && (
|
||||
<ActivityIndicator className="mr-2" size={16} />
|
||||
)}
|
||||
{isValidating
|
||||
? t("frigatePlus.apiKey.validating")
|
||||
: t("frigatePlus.apiKey.validate")}
|
||||
</Button>
|
||||
{keyStatus && !isValidating && (
|
||||
<div className="flex items-center gap-2">
|
||||
{keyStatus === "valid" ? (
|
||||
<CheckCircle2 className="size-5 shrink-0 text-green-500" />
|
||||
) : keyStatus === "unreachable" ? (
|
||||
<AlertTriangle className="size-5 shrink-0 text-yellow-500" />
|
||||
) : (
|
||||
<XCircle className="size-5 shrink-0 text-red-500" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{keyStatus === "valid"
|
||||
? t("frigatePlus.apiKey.serverValid")
|
||||
: keyStatus === "unreachable"
|
||||
? t("frigatePlus.apiKey.serverUnreachable")
|
||||
: t("frigatePlus.apiKey.serverInvalid")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{config?.plus?.enabled
|
||||
? t("frigatePlus.apiKey.validated")
|
||||
: t("frigatePlus.apiKey.notValidated")}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user