From 4cf25dc53db414a40d44f4949396e34c58b5c29c Mon Sep 17 00:00:00 2001 From: decolua Date: Wed, 18 Feb 2026 13:46:14 +0700 Subject: [PATCH] feat: implement API key requirement toggle --- .../dashboard/endpoint/EndpointPageClient.js | 30 ++++++++++++++++++- src/sse/handlers/chat.js | 11 +++---- src/sse/handlers/embeddings.js | 10 ++++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 0a6cca8c..5ab015b2 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { Card, Button, Input, Modal, CardSkeleton } from "@/shared/components"; +import { Card, Button, Input, Modal, CardSkeleton, Toggle } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; const DEFAULT_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || ""; @@ -16,6 +16,7 @@ export default function APIPageClient({ machineId }) { const [createdKey, setCreatedKey] = useState(null); // Cloud sync state + const [requireApiKey, setRequireApiKey] = useState(false); const [cloudEnabled, setCloudEnabled] = useState(false); const [cloudUrl, setCloudUrl] = useState(DEFAULT_CLOUD_URL); const [cloudUrlInput, setCloudUrlInput] = useState(DEFAULT_CLOUD_URL); @@ -63,6 +64,7 @@ export default function APIPageClient({ machineId }) { if (res.ok) { const data = await res.json(); setCloudEnabled(data.cloudEnabled || false); + setRequireApiKey(data.requireApiKey || false); const url = data.cloudUrl || DEFAULT_CLOUD_URL; setCloudUrl(url); setCloudUrlInput(url); @@ -72,6 +74,19 @@ export default function APIPageClient({ machineId }) { } }; + const handleRequireApiKey = async (value) => { + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ requireApiKey: value }), + }); + if (res.ok) setRequireApiKey(value); + } catch (error) { + console.log("Error updating requireApiKey:", error); + } + }; + const fetchData = async () => { try { const keysRes = await fetch("/api/keys"); @@ -361,6 +376,19 @@ export default function APIPageClient({ machineId }) { +
+
+

Require API key

+

+ Requests without a valid key will be rejected +

+
+ handleRequireApiKey(!requireApiKey)} + /> +
+ {keys.length === 0 ? (
diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js index cddbaac6..bc7b8c27 100644 --- a/src/sse/handlers/chat.js +++ b/src/sse/handlers/chat.js @@ -5,6 +5,7 @@ import { extractApiKey, isValidApiKey, } from "../services/auth.js"; +import { getSettings } from "@/lib/localDb"; import { getModelInfo, getComboModels } from "../services/model.js"; import { handleChatCore } from "open-sse/handlers/chatCore.js"; import { errorResponse, unavailableResponse } from "open-sse/utils/error.js"; @@ -57,16 +58,16 @@ export async function handleChat(request, clientRawRequest = null) { log.debug("AUTH", "No API key provided (local mode)"); } - // Optional strict API key mode for /v1 endpoints. - // Keep disabled by default to preserve local-mode compatibility. - if (process.env.REQUIRE_API_KEY === "true") { + // Enforce API key if enabled in settings + const settings = await getSettings(); + if (settings.requireApiKey) { if (!apiKey) { - log.warn("AUTH", "Missing API key while REQUIRE_API_KEY=true"); + log.warn("AUTH", "Missing API key (requireApiKey=true)"); return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); } const valid = await isValidApiKey(apiKey); if (!valid) { - log.warn("AUTH", "Invalid API key while REQUIRE_API_KEY=true"); + log.warn("AUTH", "Invalid API key (requireApiKey=true)"); return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); } } diff --git a/src/sse/handlers/embeddings.js b/src/sse/handlers/embeddings.js index 344df4c3..3449ea58 100644 --- a/src/sse/handlers/embeddings.js +++ b/src/sse/handlers/embeddings.js @@ -5,6 +5,7 @@ import { extractApiKey, isValidApiKey, } from "../services/auth.js"; +import { getSettings } from "@/lib/localDb"; import { getModelInfo } from "../services/model.js"; import { handleEmbeddingsCore } from "open-sse/handlers/embeddingsCore.js"; import { errorResponse, unavailableResponse } from "open-sse/utils/error.js"; @@ -40,15 +41,16 @@ export async function handleEmbeddings(request) { log.debug("AUTH", "No API key provided (local mode)"); } - // Optional strict API key validation - if (process.env.REQUIRE_API_KEY === "true") { + // Enforce API key if enabled in settings + const settings = await getSettings(); + if (settings.requireApiKey) { if (!apiKey) { - log.warn("AUTH", "Missing API key while REQUIRE_API_KEY=true"); + log.warn("AUTH", "Missing API key (requireApiKey=true)"); return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key"); } const valid = await isValidApiKey(apiKey); if (!valid) { - log.warn("AUTH", "Invalid API key while REQUIRE_API_KEY=true"); + log.warn("AUTH", "Invalid API key (requireApiKey=true)"); return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key"); } }