From b15b62cae61b3087dacfa75294dc6947db570575 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Sat, 4 Apr 2026 14:01:04 -0400 Subject: [PATCH] Fix project-route auth gaps for domain and SERP lookups (#65) --- .../domainOverviewControllerInternals.ts | 10 +++++++-- .../domain/useDomainOverviewController.ts | 2 +- .../keywords/hooks/useKeywordSerpAnalysis.ts | 8 +++++-- .../state/useKeywordResearchController.ts | 2 +- .../features/domain/services/DomainService.ts | 2 ++ .../keywords/services/research/serp.ts | 2 ++ src/serverFunctions/domain.ts | 18 ++++++++++------ src/serverFunctions/keywords.ts | 21 +++++++++++-------- src/types/schemas/domain.ts | 1 + src/types/schemas/keywords.ts | 1 + 10 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/client/features/domain/domainOverviewControllerInternals.ts b/src/client/features/domain/domainOverviewControllerInternals.ts index a316631..ab0c0cb 100644 --- a/src/client/features/domain/domainOverviewControllerInternals.ts +++ b/src/client/features/domain/domainOverviewControllerInternals.ts @@ -225,14 +225,20 @@ export function useSyncRouteState({ }, [navigate]); } -export function useDomainLookupMutation() { +export function useDomainLookupMutation(projectId: string) { return useMutation({ mutationFn: (data: { domain: string; includeSubdomains: boolean; locationCode: number; languageCode: string; - }) => getDomainOverview({ data }), + }) => + getDomainOverview({ + data: { + ...data, + projectId, + }, + }), }); } diff --git a/src/client/features/domain/useDomainOverviewController.ts b/src/client/features/domain/useDomainOverviewController.ts index 1e919ed..d225f00 100644 --- a/src/client/features/domain/useDomainOverviewController.ts +++ b/src/client/features/domain/useDomainOverviewController.ts @@ -160,7 +160,7 @@ export function useDomainOverviewController({ }); useSyncRouteState({ controlsForm, searchState, setPendingSearch, navigate }); - const domainMutation = useDomainLookupMutation(); + const domainMutation = useDomainLookupMutation(projectId); const saveMutation = useSaveKeywordsMutation({ projectId, queryClient }); const dataState = useOverviewDataState({ overview, diff --git a/src/client/features/keywords/hooks/useKeywordSerpAnalysis.ts b/src/client/features/keywords/hooks/useKeywordSerpAnalysis.ts index 3bde2f5..016fdfc 100644 --- a/src/client/features/keywords/hooks/useKeywordSerpAnalysis.ts +++ b/src/client/features/keywords/hooks/useKeywordSerpAnalysis.ts @@ -4,16 +4,20 @@ import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { getLanguageCode } from "@/client/features/keywords/utils"; import { getSerpAnalysis } from "@/serverFunctions/keywords"; -export function useKeywordSerpAnalysis(locationCode: number) { +export function useKeywordSerpAnalysis( + projectId: string, + locationCode: number, +) { const [serpKeyword, setSerpKeyword] = useState(null); const [serpPage, setSerpPage] = useState(0); const SERP_PAGE_SIZE = 10; const serpQuery = useQuery({ - queryKey: ["serpAnalysis", serpKeyword, locationCode], + queryKey: ["serpAnalysis", projectId, serpKeyword, locationCode], queryFn: () => getSerpAnalysis({ data: { + projectId, keyword: serpKeyword!, locationCode, languageCode: getLanguageCode(locationCode), diff --git a/src/client/features/keywords/state/useKeywordResearchController.ts b/src/client/features/keywords/state/useKeywordResearchController.ts index dd3a67a..11178ed 100644 --- a/src/client/features/keywords/state/useKeywordResearchController.ts +++ b/src/client/features/keywords/state/useKeywordResearchController.ts @@ -165,7 +165,7 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) { activeSerpKeyword, serpLoading, serpError, - } = useKeywordSerpAnalysis(locationCode); + } = useKeywordSerpAnalysis(input.projectId, locationCode); const { history, diff --git a/src/server/features/domain/services/DomainService.ts b/src/server/features/domain/services/DomainService.ts index 88c8ce3..b492a94 100644 --- a/src/server/features/domain/services/DomainService.ts +++ b/src/server/features/domain/services/DomainService.ts @@ -69,6 +69,7 @@ const domainOverviewSchema = z.object({ async function getOverview( input: { + projectId: string; domain: string; includeSubdomains: boolean; locationCode: number; @@ -80,6 +81,7 @@ async function getOverview( const cacheKey = await buildCacheKey("domain:overview", { organizationId: billingCustomer.organizationId, + projectId: input.projectId, domain, includeSubdomains: input.includeSubdomains, locationCode: input.locationCode, diff --git a/src/server/features/keywords/services/research/serp.ts b/src/server/features/keywords/services/research/serp.ts index f815ce2..d8b33eb 100644 --- a/src/server/features/keywords/services/research/serp.ts +++ b/src/server/features/keywords/services/research/serp.ts @@ -56,6 +56,7 @@ function mapOrganicSerpItems(items: SerpLiveItem[]): SerpResultItem[] { async function getSerpLiveAnalysis( input: { + projectId: string; keyword: string; locationCode: number; languageCode: string; @@ -66,6 +67,7 @@ async function getSerpLiveAnalysis( const cacheKey = await buildCacheKey("serp:analysis", { organizationId: billingCustomer.organizationId, + projectId: input.projectId, keyword, locationCode: input.locationCode, languageCode: input.languageCode, diff --git a/src/serverFunctions/domain.ts b/src/serverFunctions/domain.ts index 1c3a33a..64de032 100644 --- a/src/serverFunctions/domain.ts +++ b/src/serverFunctions/domain.ts @@ -1,14 +1,20 @@ import { createServerFn } from "@tanstack/react-start"; -import { requireAuthenticatedContext } from "@/serverFunctions/middleware"; +import { requireProjectContext } from "@/serverFunctions/middleware"; import { domainOverviewSchema } from "@/types/schemas/domain"; import { DomainService } from "@/server/features/domain/services/DomainService"; export const getDomainOverview = createServerFn({ method: "POST" }) - .middleware(requireAuthenticatedContext) + .middleware(requireProjectContext) .inputValidator((data: unknown) => domainOverviewSchema.parse(data)) .handler(async ({ data, context }) => - DomainService.getOverview(data, { - organizationId: context.organizationId, - userEmail: context.userEmail, - }), + DomainService.getOverview( + { + ...data, + projectId: context.project.id, + }, + { + organizationId: context.organizationId, + userEmail: context.userEmail, + }, + ), ); diff --git a/src/serverFunctions/keywords.ts b/src/serverFunctions/keywords.ts index 15a911f..5a365ca 100644 --- a/src/serverFunctions/keywords.ts +++ b/src/serverFunctions/keywords.ts @@ -7,10 +7,7 @@ import { serpAnalysisSchema, } from "@/types/schemas/keywords"; import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService"; -import { - requireAuthenticatedContext, - requireProjectContext, -} from "@/serverFunctions/middleware"; +import { requireProjectContext } from "@/serverFunctions/middleware"; export const researchKeywords = createServerFn({ method: "POST" }) .middleware(requireProjectContext) @@ -58,11 +55,17 @@ export const removeSavedKeyword = createServerFn({ }); export const getSerpAnalysis = createServerFn({ method: "POST" }) - .middleware(requireAuthenticatedContext) + .middleware(requireProjectContext) .inputValidator((data: unknown) => serpAnalysisSchema.parse(data)) .handler(async ({ data, context }) => - KeywordResearchService.getSerpAnalysis(data, { - organizationId: context.organizationId, - userEmail: context.userEmail, - }), + KeywordResearchService.getSerpAnalysis( + { + ...data, + projectId: context.project.id, + }, + { + organizationId: context.organizationId, + userEmail: context.userEmail, + }, + ), ); diff --git a/src/types/schemas/domain.ts b/src/types/schemas/domain.ts index 6b3478b..179b726 100644 --- a/src/types/schemas/domain.ts +++ b/src/types/schemas/domain.ts @@ -5,6 +5,7 @@ const booleanSearchParamSchema = z .transform((value) => value === true || value === "true"); export const domainOverviewSchema = z.object({ + projectId: z.string().min(1), domain: z.string().min(1, "Domain is required").max(255), includeSubdomains: z.boolean().default(true), locationCode: z.number().int().positive().default(2840), diff --git a/src/types/schemas/keywords.ts b/src/types/schemas/keywords.ts index c78a3c5..fe5f7c0 100644 --- a/src/types/schemas/keywords.ts +++ b/src/types/schemas/keywords.ts @@ -71,6 +71,7 @@ export type ResearchKeywordsInput = z.infer; export type SaveKeywordsInput = z.infer; export type RemoveSavedKeywordInput = z.infer; export const serpAnalysisSchema = z.object({ + projectId: z.string().min(1), keyword: z.string().min(1), locationCode: z.number().int().positive().default(2840), languageCode: z.string().min(2).max(8).default("en"),