Fix project-route auth gaps for domain and SERP lookups (#65)

This commit is contained in:
Ben Senescu 2026-04-04 14:01:04 -04:00 committed by Ben Senescu
parent 5d093c1ebb
commit b15b62cae6
10 changed files with 46 additions and 21 deletions

View File

@ -225,14 +225,20 @@ export function useSyncRouteState({
}, [navigate]); }, [navigate]);
} }
export function useDomainLookupMutation() { export function useDomainLookupMutation(projectId: string) {
return useMutation({ return useMutation({
mutationFn: (data: { mutationFn: (data: {
domain: string; domain: string;
includeSubdomains: boolean; includeSubdomains: boolean;
locationCode: number; locationCode: number;
languageCode: string; languageCode: string;
}) => getDomainOverview({ data }), }) =>
getDomainOverview({
data: {
...data,
projectId,
},
}),
}); });
} }

View File

@ -160,7 +160,7 @@ export function useDomainOverviewController({
}); });
useSyncRouteState({ controlsForm, searchState, setPendingSearch, navigate }); useSyncRouteState({ controlsForm, searchState, setPendingSearch, navigate });
const domainMutation = useDomainLookupMutation(); const domainMutation = useDomainLookupMutation(projectId);
const saveMutation = useSaveKeywordsMutation({ projectId, queryClient }); const saveMutation = useSaveKeywordsMutation({ projectId, queryClient });
const dataState = useOverviewDataState({ const dataState = useOverviewDataState({
overview, overview,

View File

@ -4,16 +4,20 @@ import { getStandardErrorMessage } from "@/client/lib/error-messages";
import { getLanguageCode } from "@/client/features/keywords/utils"; import { getLanguageCode } from "@/client/features/keywords/utils";
import { getSerpAnalysis } from "@/serverFunctions/keywords"; import { getSerpAnalysis } from "@/serverFunctions/keywords";
export function useKeywordSerpAnalysis(locationCode: number) { export function useKeywordSerpAnalysis(
projectId: string,
locationCode: number,
) {
const [serpKeyword, setSerpKeyword] = useState<string | null>(null); const [serpKeyword, setSerpKeyword] = useState<string | null>(null);
const [serpPage, setSerpPage] = useState(0); const [serpPage, setSerpPage] = useState(0);
const SERP_PAGE_SIZE = 10; const SERP_PAGE_SIZE = 10;
const serpQuery = useQuery({ const serpQuery = useQuery({
queryKey: ["serpAnalysis", serpKeyword, locationCode], queryKey: ["serpAnalysis", projectId, serpKeyword, locationCode],
queryFn: () => queryFn: () =>
getSerpAnalysis({ getSerpAnalysis({
data: { data: {
projectId,
keyword: serpKeyword!, keyword: serpKeyword!,
locationCode, locationCode,
languageCode: getLanguageCode(locationCode), languageCode: getLanguageCode(locationCode),

View File

@ -165,7 +165,7 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) {
activeSerpKeyword, activeSerpKeyword,
serpLoading, serpLoading,
serpError, serpError,
} = useKeywordSerpAnalysis(locationCode); } = useKeywordSerpAnalysis(input.projectId, locationCode);
const { const {
history, history,

View File

@ -69,6 +69,7 @@ const domainOverviewSchema = z.object({
async function getOverview( async function getOverview(
input: { input: {
projectId: string;
domain: string; domain: string;
includeSubdomains: boolean; includeSubdomains: boolean;
locationCode: number; locationCode: number;
@ -80,6 +81,7 @@ async function getOverview(
const cacheKey = await buildCacheKey("domain:overview", { const cacheKey = await buildCacheKey("domain:overview", {
organizationId: billingCustomer.organizationId, organizationId: billingCustomer.organizationId,
projectId: input.projectId,
domain, domain,
includeSubdomains: input.includeSubdomains, includeSubdomains: input.includeSubdomains,
locationCode: input.locationCode, locationCode: input.locationCode,

View File

@ -56,6 +56,7 @@ function mapOrganicSerpItems(items: SerpLiveItem[]): SerpResultItem[] {
async function getSerpLiveAnalysis( async function getSerpLiveAnalysis(
input: { input: {
projectId: string;
keyword: string; keyword: string;
locationCode: number; locationCode: number;
languageCode: string; languageCode: string;
@ -66,6 +67,7 @@ async function getSerpLiveAnalysis(
const cacheKey = await buildCacheKey("serp:analysis", { const cacheKey = await buildCacheKey("serp:analysis", {
organizationId: billingCustomer.organizationId, organizationId: billingCustomer.organizationId,
projectId: input.projectId,
keyword, keyword,
locationCode: input.locationCode, locationCode: input.locationCode,
languageCode: input.languageCode, languageCode: input.languageCode,

View File

@ -1,14 +1,20 @@
import { createServerFn } from "@tanstack/react-start"; import { createServerFn } from "@tanstack/react-start";
import { requireAuthenticatedContext } from "@/serverFunctions/middleware"; import { requireProjectContext } from "@/serverFunctions/middleware";
import { domainOverviewSchema } from "@/types/schemas/domain"; import { domainOverviewSchema } from "@/types/schemas/domain";
import { DomainService } from "@/server/features/domain/services/DomainService"; import { DomainService } from "@/server/features/domain/services/DomainService";
export const getDomainOverview = createServerFn({ method: "POST" }) export const getDomainOverview = createServerFn({ method: "POST" })
.middleware(requireAuthenticatedContext) .middleware(requireProjectContext)
.inputValidator((data: unknown) => domainOverviewSchema.parse(data)) .inputValidator((data: unknown) => domainOverviewSchema.parse(data))
.handler(async ({ data, context }) => .handler(async ({ data, context }) =>
DomainService.getOverview(data, { DomainService.getOverview(
{
...data,
projectId: context.project.id,
},
{
organizationId: context.organizationId, organizationId: context.organizationId,
userEmail: context.userEmail, userEmail: context.userEmail,
}), },
),
); );

View File

@ -7,10 +7,7 @@ import {
serpAnalysisSchema, serpAnalysisSchema,
} from "@/types/schemas/keywords"; } from "@/types/schemas/keywords";
import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService"; import { KeywordResearchService } from "@/server/features/keywords/services/KeywordResearchService";
import { import { requireProjectContext } from "@/serverFunctions/middleware";
requireAuthenticatedContext,
requireProjectContext,
} from "@/serverFunctions/middleware";
export const researchKeywords = createServerFn({ method: "POST" }) export const researchKeywords = createServerFn({ method: "POST" })
.middleware(requireProjectContext) .middleware(requireProjectContext)
@ -58,11 +55,17 @@ export const removeSavedKeyword = createServerFn({
}); });
export const getSerpAnalysis = createServerFn({ method: "POST" }) export const getSerpAnalysis = createServerFn({ method: "POST" })
.middleware(requireAuthenticatedContext) .middleware(requireProjectContext)
.inputValidator((data: unknown) => serpAnalysisSchema.parse(data)) .inputValidator((data: unknown) => serpAnalysisSchema.parse(data))
.handler(async ({ data, context }) => .handler(async ({ data, context }) =>
KeywordResearchService.getSerpAnalysis(data, { KeywordResearchService.getSerpAnalysis(
{
...data,
projectId: context.project.id,
},
{
organizationId: context.organizationId, organizationId: context.organizationId,
userEmail: context.userEmail, userEmail: context.userEmail,
}), },
),
); );

View File

@ -5,6 +5,7 @@ const booleanSearchParamSchema = z
.transform((value) => value === true || value === "true"); .transform((value) => value === true || value === "true");
export const domainOverviewSchema = z.object({ export const domainOverviewSchema = z.object({
projectId: z.string().min(1),
domain: z.string().min(1, "Domain is required").max(255), domain: z.string().min(1, "Domain is required").max(255),
includeSubdomains: z.boolean().default(true), includeSubdomains: z.boolean().default(true),
locationCode: z.number().int().positive().default(2840), locationCode: z.number().int().positive().default(2840),

View File

@ -71,6 +71,7 @@ export type ResearchKeywordsInput = z.infer<typeof researchKeywordsSchema>;
export type SaveKeywordsInput = z.infer<typeof saveKeywordsSchema>; export type SaveKeywordsInput = z.infer<typeof saveKeywordsSchema>;
export type RemoveSavedKeywordInput = z.infer<typeof removeSavedKeywordSchema>; export type RemoveSavedKeywordInput = z.infer<typeof removeSavedKeywordSchema>;
export const serpAnalysisSchema = z.object({ export const serpAnalysisSchema = z.object({
projectId: z.string().min(1),
keyword: z.string().min(1), keyword: z.string().min(1),
locationCode: z.number().int().positive().default(2840), locationCode: z.number().int().positive().default(2840),
languageCode: z.string().min(2).max(8).default("en"), languageCode: z.string().min(2).max(8).default("en"),