diff --git a/surfsense_web/components/editConnector/types.ts b/surfsense_web/components/editConnector/types.ts index 4e91ef305..c069972ba 100644 --- a/surfsense_web/components/editConnector/types.ts +++ b/surfsense_web/components/editConnector/types.ts @@ -30,6 +30,13 @@ export const editConnectorSchema = z.object({ NOTION_INTEGRATION_TOKEN: z.string().optional(), SERPER_API_KEY: z.string().optional(), TAVILY_API_KEY: z.string().optional(), + SEARXNG_HOST: z.string().optional(), + SEARXNG_API_KEY: z.string().optional(), + SEARXNG_ENGINES: z.string().optional(), + SEARXNG_CATEGORIES: z.string().optional(), + SEARXNG_LANGUAGE: z.string().optional(), + SEARXNG_SAFESEARCH: z.string().optional(), + SEARXNG_VERIFY_SSL: z.string().optional(), LINEAR_API_KEY: z.string().optional(), LINKUP_API_KEY: z.string().optional(), DISCORD_BOT_TOKEN: z.string().optional(), diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index fe90db3c8..ae8d977d7 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -16,6 +16,38 @@ import { useSearchSourceConnectors, } from "@/hooks/use-search-source-connectors"; +const normalizeListInput = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.map((item) => String(item).trim()).filter((item) => item.length > 0); + } + if (typeof value === "string") { + return value + .split(",") + .map((item) => item.trim()) + .filter((item) => item.length > 0); + } + return []; +}; + +const arraysEqual = (a: string[], b: string[]): boolean => { + if (a.length !== b.length) return false; + return a.every((value, index) => value === b[index]); +}; + +const normalizeBoolean = (value: unknown): boolean | null => { + if (typeof value === "boolean") return value; + if (typeof value === "string") { + const lowered = value.trim().toLowerCase(); + if (["true", "1", "yes", "on"].includes(lowered)) return true; + if (["false", "0", "no", "off"].includes(lowered)) return false; + } + if (typeof value === "number") { + if (value === 1) return true; + if (value === 0) return false; + } + return null; +}; + export function useConnectorEditPage(connectorId: number, searchSpaceId: string) { const router = useRouter(); const { @@ -48,6 +80,13 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) NOTION_INTEGRATION_TOKEN: "", SERPER_API_KEY: "", TAVILY_API_KEY: "", + SEARXNG_HOST: "", + SEARXNG_API_KEY: "", + SEARXNG_ENGINES: "", + SEARXNG_CATEGORIES: "", + SEARXNG_LANGUAGE: "", + SEARXNG_SAFESEARCH: "", + SEARXNG_VERIFY_SSL: "", LINEAR_API_KEY: "", DISCORD_BOT_TOKEN: "", CONFLUENCE_BASE_URL: "", @@ -74,6 +113,23 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "", SERPER_API_KEY: config.SERPER_API_KEY || "", TAVILY_API_KEY: config.TAVILY_API_KEY || "", + SEARXNG_HOST: config.SEARXNG_HOST || "", + SEARXNG_API_KEY: config.SEARXNG_API_KEY || "", + SEARXNG_ENGINES: Array.isArray(config.SEARXNG_ENGINES) + ? config.SEARXNG_ENGINES.join(", ") + : config.SEARXNG_ENGINES || "", + SEARXNG_CATEGORIES: Array.isArray(config.SEARXNG_CATEGORIES) + ? config.SEARXNG_CATEGORIES.join(", ") + : config.SEARXNG_CATEGORIES || "", + SEARXNG_LANGUAGE: config.SEARXNG_LANGUAGE || "", + SEARXNG_SAFESEARCH: + config.SEARXNG_SAFESEARCH !== undefined && config.SEARXNG_SAFESEARCH !== null + ? String(config.SEARXNG_SAFESEARCH) + : "", + SEARXNG_VERIFY_SSL: + config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null + ? String(config.SEARXNG_VERIFY_SSL) + : "", LINEAR_API_KEY: config.LINEAR_API_KEY || "", LINKUP_API_KEY: config.LINKUP_API_KEY || "", DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "", @@ -238,6 +294,88 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY }; } break; + case "SEARXNG_API": { + const host = (formData.SEARXNG_HOST || "").trim(); + if (!host) { + toast.error("SearxNG host is required."); + setIsSaving(false); + return; + } + + const candidateConfig: Record = { SEARXNG_HOST: host }; + let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim(); + + const apiKey = (formData.SEARXNG_API_KEY || "").trim(); + const originalApiKey = (originalConfig.SEARXNG_API_KEY || "").trim(); + if (apiKey !== originalApiKey) { + candidateConfig.SEARXNG_API_KEY = apiKey || null; + hasChanges = true; + } + + const newEngines = normalizeListInput(formData.SEARXNG_ENGINES || ""); + const originalEngines = normalizeListInput(originalConfig.SEARXNG_ENGINES); + if (!arraysEqual(newEngines, originalEngines)) { + candidateConfig.SEARXNG_ENGINES = newEngines; + hasChanges = true; + } + + const newCategories = normalizeListInput(formData.SEARXNG_CATEGORIES || ""); + const originalCategories = normalizeListInput(originalConfig.SEARXNG_CATEGORIES); + if (!arraysEqual(newCategories, originalCategories)) { + candidateConfig.SEARXNG_CATEGORIES = newCategories; + hasChanges = true; + } + + const language = (formData.SEARXNG_LANGUAGE || "").trim(); + const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim(); + if (language !== originalLanguage) { + candidateConfig.SEARXNG_LANGUAGE = language || null; + hasChanges = true; + } + + const safesearchRaw = (formData.SEARXNG_SAFESEARCH || "").trim(); + const originalSafesearch = originalConfig.SEARXNG_SAFESEARCH; + if (safesearchRaw) { + const parsed = Number(safesearchRaw); + if (Number.isNaN(parsed)) { + toast.error("SearxNG SafeSearch must be a number."); + setIsSaving(false); + return; + } + if (parsed !== Number(originalSafesearch)) { + candidateConfig.SEARXNG_SAFESEARCH = parsed; + hasChanges = true; + } + } else if (originalSafesearch !== undefined && originalSafesearch !== null) { + candidateConfig.SEARXNG_SAFESEARCH = null; + hasChanges = true; + } + + const verifyRaw = (formData.SEARXNG_VERIFY_SSL || "").trim().toLowerCase(); + const originalVerifyBool = normalizeBoolean(originalConfig.SEARXNG_VERIFY_SSL); + if (verifyRaw) { + let parsedBool: boolean | null = null; + if (["true", "1", "yes", "on"].includes(verifyRaw)) parsedBool = true; + else if (["false", "0", "no", "off"].includes(verifyRaw)) parsedBool = false; + if (parsedBool === null) { + toast.error("SearxNG SSL verification must be true or false."); + setIsSaving(false); + return; + } + if (parsedBool !== originalVerifyBool) { + candidateConfig.SEARXNG_VERIFY_SSL = parsedBool; + hasChanges = true; + } + } else if (originalVerifyBool !== null) { + candidateConfig.SEARXNG_VERIFY_SSL = null; + hasChanges = true; + } + + if (hasChanges) { + newConfig = candidateConfig; + } + break; + } case "LINEAR_CONNECTOR": if (formData.LINEAR_API_KEY !== originalConfig.LINEAR_API_KEY) { @@ -367,6 +505,33 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) editForm.setValue("SERPER_API_KEY", newlySavedConfig.SERPER_API_KEY || ""); } else if (connector.connector_type === "TAVILY_API") { editForm.setValue("TAVILY_API_KEY", newlySavedConfig.TAVILY_API_KEY || ""); + } else if (connector.connector_type === "SEARXNG_API") { + editForm.setValue("SEARXNG_HOST", newlySavedConfig.SEARXNG_HOST || ""); + editForm.setValue("SEARXNG_API_KEY", newlySavedConfig.SEARXNG_API_KEY || ""); + editForm.setValue( + "SEARXNG_ENGINES", + normalizeListInput(newlySavedConfig.SEARXNG_ENGINES).join(", ") + ); + editForm.setValue( + "SEARXNG_CATEGORIES", + normalizeListInput(newlySavedConfig.SEARXNG_CATEGORIES).join(", ") + ); + editForm.setValue( + "SEARXNG_LANGUAGE", + newlySavedConfig.SEARXNG_LANGUAGE || "" + ); + editForm.setValue( + "SEARXNG_SAFESEARCH", + newlySavedConfig.SEARXNG_SAFESEARCH === null || + newlySavedConfig.SEARXNG_SAFESEARCH === undefined + ? "" + : String(newlySavedConfig.SEARXNG_SAFESEARCH) + ); + const verifyValue = normalizeBoolean(newlySavedConfig.SEARXNG_VERIFY_SSL); + editForm.setValue( + "SEARXNG_VERIFY_SSL", + verifyValue === null ? "" : String(verifyValue) + ); } else if (connector.connector_type === "LINEAR_CONNECTOR") { editForm.setValue("LINEAR_API_KEY", newlySavedConfig.LINEAR_API_KEY || ""); } else if (connector.connector_type === "LINKUP_API") {