From aa86534a5247254a05e472a1187db00f42d327de Mon Sep 17 00:00:00 2001 From: suryo12 Date: Sat, 23 May 2026 23:22:18 +0700 Subject: [PATCH] refactor(web): centralize OAuth callback cookie contract (fixes #1362) Replace the duplicated `OAUTH_RESULT_COOKIE` constant and inline payload type across the callback route and connector dialog hook with a shared `contracts/types/oauth.types.ts` module that exports: - OAUTH_RESULT_COOKIE constant - oauthCallbackResultSchema Zod schema - OAuthCallbackResult type (inferred from the schema) - parseOAuthCallbackResult() helper that returns null on invalid JSON or shape mismatch The route handler now uses the shared type to constrain the cookie payload at compile time. The consumer hook validates the cookie value through the helper instead of an unchecked JSON.parse, removing the silent runtime risk when the cookie is tampered with or its shape drifts. --- .../connectors/callback/route.ts | 8 ++--- .../hooks/use-connector-dialog.ts | 18 +++-------- surfsense_web/contracts/types/oauth.types.ts | 31 +++++++++++++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 surfsense_web/contracts/types/oauth.types.ts diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts index 14573066d..304f33a33 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/callback/route.ts @@ -1,6 +1,5 @@ import { type NextRequest, NextResponse } from "next/server"; - -const OAUTH_RESULT_COOKIE = "connector_oauth_result"; +import { OAUTH_RESULT_COOKIE, type OAuthCallbackResult } from "@/contracts/types/oauth.types"; export async function GET( request: NextRequest, @@ -9,12 +8,13 @@ export async function GET( const { search_space_id } = await params; const searchParams = request.nextUrl.searchParams; - const result = JSON.stringify({ + const payload: OAuthCallbackResult = { success: searchParams.get("success"), error: searchParams.get("error"), connector: searchParams.get("connector"), connectorId: searchParams.get("connectorId"), - }); + }; + const result = JSON.stringify(payload); const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url); diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index d1d675ad1..25ab82e2e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -14,7 +14,9 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer import { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { searchSourceConnector } from "@/contracts/types/connector.types"; +import { OAUTH_RESULT_COOKIE, parseOAuthCallbackResult } from "@/contracts/types/oauth.types"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { BACKEND_URL } from "@/lib/env-config"; import { trackConnectorConnected, trackConnectorDeleted, @@ -36,15 +38,12 @@ import { OAUTH_CONNECTORS, OTHER_CONNECTORS, } from "../constants/connector-constants"; - import { dateRangeSchema, frequencyMinutesSchema, parseOAuthAuthResponse, validateIndexingConfigState, } from "../constants/connector-popup.schemas"; -import { BACKEND_URL } from "@/lib/env-config"; -const OAUTH_RESULT_COOKIE = "connector_oauth_result"; function readOAuthResultCookie(): string | null { const match = document.cookie @@ -211,17 +210,8 @@ export const useConnectorDialog = () => { if (!raw || !searchSpaceId) return; clearOAuthResultCookie(); - let result: { - success: string | null; - error: string | null; - connector: string | null; - connectorId: string | null; - }; - try { - result = JSON.parse(raw); - } catch { - return; - } + const result = parseOAuthCallbackResult(raw); + if (!result) return; if (result.error) { const oauthConnector = result.connector diff --git a/surfsense_web/contracts/types/oauth.types.ts b/surfsense_web/contracts/types/oauth.types.ts new file mode 100644 index 000000000..8b3854cf8 --- /dev/null +++ b/surfsense_web/contracts/types/oauth.types.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const OAUTH_RESULT_COOKIE = "connector_oauth_result"; + +/** + * Schema for the payload written to the `connector_oauth_result` cookie by the + * OAuth callback route and read back by the connector dialog hook. + */ +export const oauthCallbackResultSchema = z.object({ + success: z.string().nullable(), + error: z.string().nullable(), + connector: z.string().nullable(), + connectorId: z.string().nullable(), +}); + +export type OAuthCallbackResult = z.infer; + +/** + * Safely decode and validate the OAuth callback cookie value. Returns `null` + * when the value is not valid JSON or does not match the expected shape. + */ +export function parseOAuthCallbackResult(raw: string): OAuthCallbackResult | null { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + const result = oauthCallbackResultSchema.safeParse(parsed); + return result.success ? result.data : null; +}