Improve error handling + local setup (#4)
* Add unified Docker self-host stack for OpenSEO and Gateway * Fix self-host migration flow and env wiring * Track latest gateway release by default and harden migrations * Fix gateway static asset serving in self-host runtime * Run gateway self-host from source with .env-based Vite runtime * Harden self-host env mode and remove apt dependency from gateway build * Avoid gateway build ENOSPC by installing deps at runtime * Revert temporary ENOSPC workaround for gateway Docker setup * Use Cloudflare selfhost env precedence for Docker runtime * Add selfhost wrangler environment and tighten Docker docs * Stabilize selfhost Docker runtime and persistence Use full Node base images for reliable TLS trust, simplify selfhost startup/env handling, and persist local gateway wrangler state to avoid resets between restarts. * Simplify Docker selfhost: rename scripts to docker:selfhost:*, remove GATEWAY_RELEASE_REPO, default to latest gateway * Remove selfhost wrangler env, use .env.local everywhere, simplify docker script names * Simplify selfhost compose: remove D1 preflight, conditional migration, and healthcheck * Extract inline Node.js from gateway Dockerfile into standalone scripts * Save * Move OpenSEO dependency install from container startup to image build step * Move self-host docker scripts and compose into dedicated folder * save * disable TanStack devtools in Docker self-host * Make edits * clarify self-hosting env setup docs * save * save * centralize server-function error middleware and simplify client-safe messaging * remove local migration-specific error remapping Keep auth middleware simple by letting DB/schema failures flow through the centralized error handler and remove the unused LOCAL_DB_MIGRATION_REQUIRED client/server code path. * remove custom error logging in favor of plain console.error * standardize PSI error codes and sanitize non-Error throws
This commit is contained in:
parent
4350b12c67
commit
35700ca80a
@ -170,6 +170,8 @@ cp .env.example .env.local
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
|
# Initialize local DB schema (required on a fresh machine)
|
||||||
|
pnpm run db:migrate:local
|
||||||
# This runs in BYPASS_GATEWAY mode so that you don't need to set up the Every App gateway. This is fine for local use.
|
# This runs in BYPASS_GATEWAY mode so that you don't need to set up the Every App gateway. This is fine for local use.
|
||||||
pnpm dev:agents
|
pnpm dev:agents
|
||||||
```
|
```
|
||||||
@ -184,6 +186,8 @@ Running locally is the fastest way to test core flows. In the future, local mode
|
|||||||
# This log file make it easier for your coding agent to debug.
|
# This log file make it easier for your coding agent to debug.
|
||||||
mkdir .logs
|
mkdir .logs
|
||||||
touch .logs/dev-server.log
|
touch .logs/dev-server.log
|
||||||
|
# Run once per fresh local DB
|
||||||
|
pnpm run db:migrate:local
|
||||||
# terminal 1: start once and keep running
|
# terminal 1: start once and keep running
|
||||||
pnpm dev:agents
|
pnpm dev:agents
|
||||||
```
|
```
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
"src/server.ts",
|
"src/server.ts",
|
||||||
"src/server/lib/audit/progress-kv.ts",
|
"src/server/lib/audit/progress-kv.ts",
|
||||||
"src/server/lib/audit/types.ts",
|
"src/server/lib/audit/types.ts",
|
||||||
"src/server/lib/errors.ts",
|
|
||||||
"src/server/services/PsiIssuesService.ts",
|
"src/server/services/PsiIssuesService.ts",
|
||||||
"src/server/workflows/SiteAuditWorkflow.ts",
|
"src/server/workflows/SiteAuditWorkflow.ts",
|
||||||
"src/serverFunctions/keywords.ts",
|
"src/serverFunctions/keywords.ts",
|
||||||
@ -29,7 +28,6 @@
|
|||||||
"src/types/schemas/psi.ts",
|
"src/types/schemas/psi.ts",
|
||||||
],
|
],
|
||||||
"ignoreFiles": [
|
"ignoreFiles": [
|
||||||
"src/server/lib/serverFnErrorBoundary.ts",
|
|
||||||
"src/server/services/keyword-research/helpers.ts",
|
"src/server/services/keyword-research/helpers.ts",
|
||||||
"src/server/services/keyword-research/projects.ts",
|
"src/server/services/keyword-research/projects.ts",
|
||||||
"src/server/services/keyword-research/research-data.ts",
|
"src/server/services/keyword-research/research-data.ts",
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"dev:agents": "BYPASS_GATEWAY_LOCAL_ONLY=true vite dev 2>&1 | tee .logs/dev-server.log",
|
"dev:agents": "mkdir -p .logs && BYPASS_GATEWAY_LOCAL_ONLY=true vite dev 2>&1 | tee .logs/dev-server.log",
|
||||||
"build": "vite build && tsc --noEmit",
|
"build": "vite build && tsc --noEmit",
|
||||||
"lint": "oxlint .",
|
"lint": "oxlint .",
|
||||||
"lint:fix": "oxlint . --fix",
|
"lint:fix": "oxlint . --fix",
|
||||||
|
|||||||
@ -8,7 +8,8 @@ const STANDARD_MESSAGES: Record<ErrorCode, string> = {
|
|||||||
CRAWL_TARGET_BLOCKED: "This crawl target is blocked by security policy.",
|
CRAWL_TARGET_BLOCKED: "This crawl target is blocked by security policy.",
|
||||||
RATE_LIMITED: "Too many requests. Please wait and try again.",
|
RATE_LIMITED: "Too many requests. Please wait and try again.",
|
||||||
CONFLICT: "This request conflicts with existing data.",
|
CONFLICT: "This request conflicts with existing data.",
|
||||||
INTERNAL_ERROR: "Something went wrong. Please try again.",
|
INTERNAL_ERROR:
|
||||||
|
"An unexpected error occurred. Please check server logs and try again.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getStandardErrorMessage(
|
export function getStandardErrorMessage(
|
||||||
|
|||||||
@ -7,22 +7,16 @@ import {
|
|||||||
getAuthConfig,
|
getAuthConfig,
|
||||||
} from "@every-app/sdk/tanstack/server";
|
} from "@every-app/sdk/tanstack/server";
|
||||||
import { AppError } from "@/server/lib/errors";
|
import { AppError } from "@/server/lib/errors";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
|
|
||||||
export const ensureUserMiddleware = createMiddleware({
|
export const ensureUserMiddleware = createMiddleware({
|
||||||
type: "function",
|
type: "function",
|
||||||
}).server(async (c) => {
|
}).server(async (c) => {
|
||||||
const { next } = c;
|
const { next } = c;
|
||||||
|
|
||||||
const authConfig = getAuthConfig();
|
const authConfig = getAuthConfig();
|
||||||
|
|
||||||
const session = await authenticateRequest(authConfig);
|
const session = await authenticateRequest(authConfig);
|
||||||
|
|
||||||
if (!session) {
|
if (!session || !session.email) {
|
||||||
throw new AppError("UNAUTHENTICATED");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session.email) {
|
|
||||||
throw new AppError("UNAUTHENTICATED");
|
throw new AppError("UNAUTHENTICATED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,23 +28,16 @@ export const ensureUserMiddleware = createMiddleware({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
try {
|
await db.insert(users).values({
|
||||||
await db.insert(users).values({
|
id: userId,
|
||||||
id: userId,
|
email: session.email,
|
||||||
email: session.email,
|
});
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("auth.ensure-user.create", error, { userId });
|
|
||||||
throw new AppError("INTERNAL_ERROR");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userEmail = user?.email || session.email;
|
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
context: {
|
context: {
|
||||||
userId,
|
userId,
|
||||||
userEmail,
|
userEmail: user?.email || session.email,
|
||||||
session,
|
session,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
24
src/middleware/errorHandling.ts
Normal file
24
src/middleware/errorHandling.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createMiddleware } from "@tanstack/react-start";
|
||||||
|
import { asAppError, toClientError } from "@/server/lib/errors";
|
||||||
|
|
||||||
|
export const errorHandlingMiddleware = createMiddleware({
|
||||||
|
type: "function",
|
||||||
|
}).server(async (c) => {
|
||||||
|
const { next } = c;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await next();
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
throw new Error("INTERNAL_ERROR", { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appError = asAppError(error);
|
||||||
|
|
||||||
|
if (appError?.code !== "UNAUTHENTICATED") {
|
||||||
|
console.error("server.function error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw toClientError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -29,7 +29,10 @@ function IndexRedirect() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<p className="text-error">
|
<p className="text-error">
|
||||||
{getStandardErrorMessage(error, "Failed to load. Please try again.")}
|
{getStandardErrorMessage(
|
||||||
|
error,
|
||||||
|
"An unexpected error occurred. Please check server logs.",
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export function asAppError(error: unknown): AppError | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toErrorCode(error: unknown): ErrorCode {
|
function toErrorCode(error: unknown): ErrorCode {
|
||||||
return asAppError(error)?.code ?? "INTERNAL_ERROR";
|
return asAppError(error)?.code ?? "INTERNAL_ERROR";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import { toErrorCode } from "@/server/lib/errors";
|
|
||||||
|
|
||||||
type LogContext = Record<string, unknown>;
|
|
||||||
|
|
||||||
const SENSITIVE_KEY_PATTERN = /token|secret|password|key|email|authorization/i;
|
|
||||||
|
|
||||||
function sanitizeValue(key: string, value: unknown): unknown {
|
|
||||||
if (SENSITIVE_KEY_PATTERN.test(key)) return "[REDACTED]";
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.length > 300 ? `${value.slice(0, 300)}...` : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.slice(0, 10).map((item) => sanitizeValue(key, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && typeof value === "object") {
|
|
||||||
const output: Record<string, unknown> = {};
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
|
||||||
output[k] = sanitizeValue(k, v);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeContext(context: LogContext): LogContext {
|
|
||||||
const safe: LogContext = {};
|
|
||||||
for (const [key, value] of Object.entries(context)) {
|
|
||||||
safe[key] = sanitizeValue(key, value);
|
|
||||||
}
|
|
||||||
return safe;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logServerError(
|
|
||||||
operation: string,
|
|
||||||
error: unknown,
|
|
||||||
context: LogContext = {},
|
|
||||||
): void {
|
|
||||||
const code = toErrorCode(error);
|
|
||||||
const safeErrorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? sanitizeValue("message", error.message)
|
|
||||||
: "unknown";
|
|
||||||
const safeStack =
|
|
||||||
error instanceof Error && typeof error.stack === "string"
|
|
||||||
? sanitizeValue("stack", error.stack)
|
|
||||||
: undefined;
|
|
||||||
const safeCause =
|
|
||||||
error instanceof Error && "cause" in error
|
|
||||||
? sanitizeValue("cause", (error as { cause?: unknown }).cause)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
JSON.stringify({
|
|
||||||
level: "error",
|
|
||||||
operation,
|
|
||||||
code,
|
|
||||||
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
||||||
message: safeErrorMessage,
|
|
||||||
stack: safeStack,
|
|
||||||
cause: safeCause,
|
|
||||||
context: sanitizeContext(context),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
export class PublicServerError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "PublicServerError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ErrorBoundaryOptions<TArgs> {
|
|
||||||
fallbackMessage?: string;
|
|
||||||
passThroughMessages?: string[];
|
|
||||||
getLogContext?: (args: TArgs) => Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withServerFnErrorBoundary<TArgs, TResult>(
|
|
||||||
operation: string,
|
|
||||||
handler: (args: TArgs) => Promise<TResult>,
|
|
||||||
options: ErrorBoundaryOptions<TArgs> = {},
|
|
||||||
) {
|
|
||||||
const passThroughMessages = new Set(options.passThroughMessages ?? []);
|
|
||||||
|
|
||||||
return async (args: TArgs): Promise<TResult> => {
|
|
||||||
try {
|
|
||||||
return await handler(args);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof PublicServerError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error && passThroughMessages.has(error.message)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
const cause =
|
|
||||||
error instanceof Error && "cause" in error
|
|
||||||
? (error as { cause?: unknown }).cause
|
|
||||||
: undefined;
|
|
||||||
const logContext = options.getLogContext?.(args);
|
|
||||||
console.error(`${operation} failed`, {
|
|
||||||
message,
|
|
||||||
cause,
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
...logContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new PublicServerError(
|
|
||||||
options.fallbackMessage ?? "Something went wrong. Please try again.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -7,7 +7,6 @@ import {
|
|||||||
} from "@/server/lib/dataforseo";
|
} from "@/server/lib/dataforseo";
|
||||||
import { sortBy } from "remeda";
|
import { sortBy } from "remeda";
|
||||||
import { buildCacheKey, getCached, setCached } from "@/server/lib/kv-cache";
|
import { buildCacheKey, getCached, setCached } from "@/server/lib/kv-cache";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
|
|
||||||
/** Domain overview data is refreshed every 12 hours. */
|
/** Domain overview data is refreshed every 12 hours. */
|
||||||
const DOMAIN_OVERVIEW_TTL_SECONDS = 12 * 60 * 60;
|
const DOMAIN_OVERVIEW_TTL_SECONDS = 12 * 60 * 60;
|
||||||
@ -111,11 +110,7 @@ async function getOverview(input: {
|
|||||||
if (result.hasData) {
|
if (result.hasData) {
|
||||||
void setCached(cacheKey, result, DOMAIN_OVERVIEW_TTL_SECONDS).catch(
|
void setCached(cacheKey, result, DOMAIN_OVERVIEW_TTL_SECONDS).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
logServerError("domain.overview.cache-write", error, {
|
console.error("domain.overview.cache-write failed:", error);
|
||||||
domain,
|
|
||||||
locationCode: input.locationCode,
|
|
||||||
languageCode: input.languageCode,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,6 @@ import {
|
|||||||
} from "@/server/lib/kv-cache";
|
} from "@/server/lib/kv-cache";
|
||||||
import { KeywordResearchRepository } from "@/server/repositories/KeywordResearchRepository";
|
import { KeywordResearchRepository } from "@/server/repositories/KeywordResearchRepository";
|
||||||
import { AppError } from "@/server/lib/errors";
|
import { AppError } from "@/server/lib/errors";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@ -64,15 +63,12 @@ type EnrichedKeyword = {
|
|||||||
|
|
||||||
type KeywordSource = "related" | "suggestions" | "ideas";
|
type KeywordSource = "related" | "suggestions" | "ideas";
|
||||||
|
|
||||||
function parseMonthlySearches(
|
function parseMonthlySearches(payload: string | null): MonthlySearch[] {
|
||||||
payload: string | null,
|
|
||||||
context: { keyword: string; projectId: string },
|
|
||||||
): MonthlySearch[] {
|
|
||||||
if (!payload) return [];
|
if (!payload) return [];
|
||||||
try {
|
try {
|
||||||
return JSON.parse(payload) as MonthlySearch[];
|
return JSON.parse(payload) as MonthlySearch[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logServerError("keywords.saved.parse-monthly-searches", error, context);
|
console.error("keywords.saved.parse-monthly-searches failed:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,11 +298,7 @@ async function research(
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
logServerError("keywords.research.persist-metrics", error, {
|
console.error("keywords.research.persist-metrics failed:", error);
|
||||||
locationCode: input.locationCode,
|
|
||||||
languageCode: input.languageCode,
|
|
||||||
rowCount: rows.length,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { rows, source, usedFallback };
|
return { rows, source, usedFallback };
|
||||||
@ -428,10 +420,7 @@ async function getSavedKeywords(
|
|||||||
competition: metric?.competition ?? null,
|
competition: metric?.competition ?? null,
|
||||||
keywordDifficulty: metric?.keywordDifficulty ?? null,
|
keywordDifficulty: metric?.keywordDifficulty ?? null,
|
||||||
intent: metric?.intent ?? null,
|
intent: metric?.intent ?? null,
|
||||||
monthlySearches: parseMonthlySearches(metric?.monthlySearches ?? null, {
|
monthlySearches: parseMonthlySearches(metric?.monthlySearches ?? null),
|
||||||
keyword: row.keyword,
|
|
||||||
projectId: row.projectId,
|
|
||||||
}),
|
|
||||||
fetchedAt: metric?.fetchedAt ?? null,
|
fetchedAt: metric?.fetchedAt ?? null,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,17 +7,13 @@ import type {
|
|||||||
} from "@/types/schemas/keywords";
|
} from "@/types/schemas/keywords";
|
||||||
import type { MonthlySearch, SavedKeywordRow } from "@/types/keywords";
|
import type { MonthlySearch, SavedKeywordRow } from "@/types/keywords";
|
||||||
import { normalizeKeyword } from "./helpers";
|
import { normalizeKeyword } from "./helpers";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
|
|
||||||
function parseMonthlySearches(
|
function parseMonthlySearches(payload: string | null): MonthlySearch[] {
|
||||||
payload: string | null,
|
|
||||||
context: { keyword: string; projectId: string },
|
|
||||||
): MonthlySearch[] {
|
|
||||||
if (!payload) return [];
|
if (!payload) return [];
|
||||||
try {
|
try {
|
||||||
return JSON.parse(payload) as MonthlySearch[];
|
return JSON.parse(payload) as MonthlySearch[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logServerError("keywords.saved.parse-monthly-searches", error, context);
|
console.error("keywords.saved.parse-monthly-searches failed:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,10 +72,7 @@ export async function getSavedKeywords(
|
|||||||
competition: metric?.competition ?? null,
|
competition: metric?.competition ?? null,
|
||||||
keywordDifficulty: metric?.keywordDifficulty ?? null,
|
keywordDifficulty: metric?.keywordDifficulty ?? null,
|
||||||
intent: metric?.intent ?? null,
|
intent: metric?.intent ?? null,
|
||||||
monthlySearches: parseMonthlySearches(metric?.monthlySearches ?? null, {
|
monthlySearches: parseMonthlySearches(metric?.monthlySearches ?? null),
|
||||||
keyword: row.keyword,
|
|
||||||
projectId: row.projectId,
|
|
||||||
}),
|
|
||||||
fetchedAt: metric?.fetchedAt ?? null,
|
fetchedAt: metric?.fetchedAt ?? null,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { fetchHistoricalSerpsRaw } from "@/server/lib/dataforseo";
|
import { fetchHistoricalSerpsRaw } from "@/server/lib/dataforseo";
|
||||||
import { buildCacheKey, getCached, setCached } from "@/server/lib/kv-cache";
|
import { buildCacheKey, getCached, setCached } from "@/server/lib/kv-cache";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
import type { SerpResultItem } from "@/types/keywords";
|
import type { SerpResultItem } from "@/types/keywords";
|
||||||
import { normalizeKeyword } from "./helpers";
|
import { normalizeKeyword } from "./helpers";
|
||||||
|
|
||||||
@ -57,11 +57,7 @@ export async function getSerpAnalysis(input: {
|
|||||||
|
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
void setCached(cacheKey, result, SERP_CACHE_TTL_SECONDS).catch((error) => {
|
void setCached(cacheKey, result, SERP_CACHE_TTL_SECONDS).catch((error) => {
|
||||||
logServerError("keywords.serp.cache-write", error, {
|
console.error("keywords.serp.cache-write failed:", error);
|
||||||
keyword,
|
|
||||||
locationCode: input.locationCode,
|
|
||||||
languageCode: input.languageCode,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
import { authenticatedServerFunctionMiddleware } from "@/serverFunctions/middleware";
|
||||||
import { useSessionTokenClientMiddleware } from "@every-app/sdk/tanstack";
|
|
||||||
import {
|
import {
|
||||||
startAuditSchema,
|
startAuditSchema,
|
||||||
getAuditStatusSchema,
|
getAuditStatusSchema,
|
||||||
@ -10,103 +9,53 @@ import {
|
|||||||
getCrawlProgressSchema,
|
getCrawlProgressSchema,
|
||||||
} from "@/types/schemas/audit";
|
} from "@/types/schemas/audit";
|
||||||
import { AuditService } from "@/server/services/AuditService";
|
import { AuditService } from "@/server/services/AuditService";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
import { toClientError } from "@/server/lib/errors";
|
|
||||||
|
|
||||||
export const startAudit = createServerFn({ method: "POST" })
|
export const startAudit = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => startAuditSchema.parse(data))
|
.inputValidator((data: unknown) => startAuditSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
AuditService.startAudit({
|
||||||
return await AuditService.startAudit({
|
userId: context.userId,
|
||||||
userId: context.userId,
|
projectId: data.projectId,
|
||||||
projectId: data.projectId,
|
startUrl: data.startUrl,
|
||||||
startUrl: data.startUrl,
|
maxPages: data.maxPages,
|
||||||
maxPages: data.maxPages,
|
psiStrategy: data.psiStrategy,
|
||||||
psiStrategy: data.psiStrategy,
|
psiApiKey: data.psiApiKey,
|
||||||
psiApiKey: data.psiApiKey,
|
}),
|
||||||
});
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.start", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAuditStatus = createServerFn({ method: "POST" })
|
export const getAuditStatus = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getAuditStatusSchema.parse(data))
|
.inputValidator((data: unknown) => getAuditStatusSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
AuditService.getStatus(data.auditId, context.userId),
|
||||||
return await AuditService.getStatus(data.auditId, context.userId);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.status", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
auditId: data.auditId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAuditResults = createServerFn({ method: "POST" })
|
export const getAuditResults = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getAuditResultsSchema.parse(data))
|
.inputValidator((data: unknown) => getAuditResultsSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
AuditService.getResults(data.auditId, context.userId),
|
||||||
return await AuditService.getResults(data.auditId, context.userId);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.results", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
auditId: data.auditId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getAuditHistory = createServerFn({ method: "POST" })
|
export const getAuditHistory = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getAuditHistorySchema.parse(data))
|
.inputValidator((data: unknown) => getAuditHistorySchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
AuditService.getHistory(data.projectId, context.userId),
|
||||||
return await AuditService.getHistory(data.projectId, context.userId);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.history", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getCrawlProgress = createServerFn({ method: "POST" })
|
export const getCrawlProgress = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getCrawlProgressSchema.parse(data))
|
.inputValidator((data: unknown) => getCrawlProgressSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
AuditService.getCrawlProgress(data.auditId, context.userId),
|
||||||
return await AuditService.getCrawlProgress(data.auditId, context.userId);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.crawl-progress", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
auditId: data.auditId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteAudit = createServerFn({ method: "POST" })
|
export const deleteAudit = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => deleteAuditSchema.parse(data))
|
.inputValidator((data: unknown) => deleteAuditSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
try {
|
await AuditService.remove(data.auditId, context.userId);
|
||||||
await AuditService.remove(data.auditId, context.userId);
|
return { success: true };
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("audit.delete", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
auditId: data.auditId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,22 +1,9 @@
|
|||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
import { authenticatedServerFunctionMiddleware } from "@/serverFunctions/middleware";
|
||||||
import { useSessionTokenClientMiddleware } from "@every-app/sdk/tanstack";
|
|
||||||
import { domainOverviewSchema } from "@/types/schemas/domain";
|
import { domainOverviewSchema } from "@/types/schemas/domain";
|
||||||
import { DomainService } from "@/server/services/DomainService";
|
import { DomainService } from "@/server/services/DomainService";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
import { toClientError } from "@/server/lib/errors";
|
|
||||||
|
|
||||||
export const getDomainOverview = createServerFn({ method: "POST" })
|
export const getDomainOverview = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => domainOverviewSchema.parse(data))
|
.inputValidator((data: unknown) => domainOverviewSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data }) => DomainService.getOverview(data));
|
||||||
try {
|
|
||||||
return await DomainService.getOverview(data);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("domain.overview", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
domain: data.domain,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
import { authenticatedServerFunctionMiddleware } from "@/serverFunctions/middleware";
|
||||||
import { useSessionTokenClientMiddleware } from "@every-app/sdk/tanstack";
|
|
||||||
import {
|
import {
|
||||||
researchKeywordsSchema,
|
researchKeywordsSchema,
|
||||||
createProjectSchema,
|
createProjectSchema,
|
||||||
@ -12,157 +11,73 @@ import {
|
|||||||
serpAnalysisSchema,
|
serpAnalysisSchema,
|
||||||
} from "@/types/schemas/keywords";
|
} from "@/types/schemas/keywords";
|
||||||
import { KeywordResearchService } from "@/server/services/KeywordResearchService";
|
import { KeywordResearchService } from "@/server/services/KeywordResearchService";
|
||||||
import { logServerError } from "@/server/lib/logger";
|
|
||||||
import { toClientError } from "@/server/lib/errors";
|
|
||||||
|
|
||||||
export const researchKeywords = createServerFn({ method: "POST" })
|
export const researchKeywords = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => researchKeywordsSchema.parse(data))
|
.inputValidator((data: unknown) => researchKeywordsSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.research(context.userId, data),
|
||||||
return await KeywordResearchService.research(context.userId, data);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("keywords.research", error, { userId: context.userId });
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listProjects = createServerFn({ method: "POST" })
|
export const listProjects = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.handler(async ({ context }) => {
|
.handler(async ({ context }) =>
|
||||||
try {
|
KeywordResearchService.listProjects(context.userId),
|
||||||
return await KeywordResearchService.listProjects(context.userId);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("projects.list", error, { userId: context.userId });
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createProject = createServerFn({ method: "POST" })
|
export const createProject = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => createProjectSchema.parse(data))
|
.inputValidator((data: unknown) => createProjectSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.createProject(context.userId, data),
|
||||||
return await KeywordResearchService.createProject(context.userId, data);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("projects.create", error, { userId: context.userId });
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteProject = createServerFn({ method: "POST" })
|
export const deleteProject = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => deleteProjectSchema.parse(data))
|
.inputValidator((data: unknown) => deleteProjectSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.deleteProject(context.userId, data),
|
||||||
return await KeywordResearchService.deleteProject(context.userId, data);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("projects.delete", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
export const saveKeywords = createServerFn({ method: "POST" })
|
export const saveKeywords = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => saveKeywordsSchema.parse(data))
|
.inputValidator((data: unknown) => saveKeywordsSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.saveKeywords(context.userId, data),
|
||||||
return await KeywordResearchService.saveKeywords(context.userId, data);
|
);
|
||||||
} catch (error) {
|
|
||||||
logServerError("keywords.save", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSavedKeywords = createServerFn({ method: "POST" })
|
export const getSavedKeywords = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getSavedKeywordsSchema.parse(data))
|
.inputValidator((data: unknown) => getSavedKeywordsSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.getSavedKeywords(context.userId, data),
|
||||||
return await KeywordResearchService.getSavedKeywords(
|
);
|
||||||
context.userId,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("keywords.saved.list", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeSavedKeyword = createServerFn({ method: "POST" })
|
export const removeSavedKeyword = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => removeSavedKeywordSchema.parse(data))
|
.inputValidator((data: unknown) => removeSavedKeywordSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.removeSavedKeyword(context.userId, data),
|
||||||
return await KeywordResearchService.removeSavedKeyword(
|
);
|
||||||
context.userId,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("keywords.saved.remove", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
savedKeywordId: data.savedKeywordId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getOrCreateDefaultProject = createServerFn({ method: "POST" })
|
export const getOrCreateDefaultProject = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.handler(async ({ context }) => {
|
.handler(async ({ context }) =>
|
||||||
try {
|
KeywordResearchService.getOrCreateDefaultProject(context.userId),
|
||||||
return await KeywordResearchService.getOrCreateDefaultProject(
|
);
|
||||||
context.userId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("projects.get-or-create-default", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSerpAnalysis = createServerFn({ method: "POST" })
|
export const getSerpAnalysis = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => serpAnalysisSchema.parse(data))
|
.inputValidator((data: unknown) => serpAnalysisSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data }) => KeywordResearchService.getSerpAnalysis(data));
|
||||||
try {
|
|
||||||
return await KeywordResearchService.getSerpAnalysis(data);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("keywords.serp-analysis", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
keyword: data.keyword,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProjectSchema = z.object({
|
const getProjectSchema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getProject = createServerFn({ method: "POST" })
|
export const getProject = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => getProjectSchema.parse(data))
|
.inputValidator((data: unknown) => getProjectSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) =>
|
||||||
try {
|
KeywordResearchService.getProject(context.userId, data.projectId),
|
||||||
return await KeywordResearchService.getProject(
|
);
|
||||||
context.userId,
|
|
||||||
data.projectId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logServerError("projects.get", error, {
|
|
||||||
userId: context.userId,
|
|
||||||
projectId: data.projectId,
|
|
||||||
});
|
|
||||||
throw toClientError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
9
src/serverFunctions/middleware.ts
Normal file
9
src/serverFunctions/middleware.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useSessionTokenClientMiddleware } from "@every-app/sdk/tanstack";
|
||||||
|
import { errorHandlingMiddleware } from "@/middleware/errorHandling";
|
||||||
|
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
||||||
|
|
||||||
|
export const authenticatedServerFunctionMiddleware = [
|
||||||
|
errorHandlingMiddleware,
|
||||||
|
useSessionTokenClientMiddleware,
|
||||||
|
ensureUserMiddleware,
|
||||||
|
] as const;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { createServerFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { ensureUserMiddleware } from "@/middleware/ensureUser";
|
import { authenticatedServerFunctionMiddleware } from "@/serverFunctions/middleware";
|
||||||
import { useSessionTokenClientMiddleware } from "@every-app/sdk/tanstack";
|
import { AppError } from "@/server/lib/errors";
|
||||||
import {
|
import {
|
||||||
psiAuditSchema,
|
psiAuditSchema,
|
||||||
psiAuditListSchema,
|
psiAuditListSchema,
|
||||||
@ -33,7 +33,7 @@ async function resolvePsiSource(input: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error("Audit not found");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -52,7 +52,7 @@ async function resolvePsiSource(input: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!site) {
|
if (!site) {
|
||||||
throw new Error("Audit not found");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -65,7 +65,7 @@ async function resolvePsiSource(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const runPsiAudit = createServerFn({ method: "POST" })
|
export const runPsiAudit = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiAuditSchema.parse(data))
|
.inputValidator((data: unknown) => psiAuditSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const apiKey = await KeywordResearchRepository.getProjectPsiApiKey(
|
const apiKey = await KeywordResearchRepository.getProjectPsiApiKey(
|
||||||
@ -74,9 +74,7 @@ export const runPsiAudit = createServerFn({ method: "POST" })
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(
|
throw new AppError("VALIDATION_ERROR");
|
||||||
"PSI API key is not set for this project. Save a key first.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auditId = crypto.randomUUID();
|
const auditId = crypto.randomUUID();
|
||||||
@ -146,7 +144,7 @@ export const runPsiAudit = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getProjectPsiApiKey = createServerFn({ method: "POST" })
|
export const getProjectPsiApiKey = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiProjectSchema.parse(data))
|
.inputValidator((data: unknown) => psiProjectSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
// This PSI key is intentionally treated as low-sensitivity operational config
|
// This PSI key is intentionally treated as low-sensitivity operational config
|
||||||
@ -159,7 +157,7 @@ export const getProjectPsiApiKey = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const saveProjectPsiApiKey = createServerFn({ method: "POST" })
|
export const saveProjectPsiApiKey = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiProjectKeySchema.parse(data))
|
.inputValidator((data: unknown) => psiProjectKeySchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
// Same tradeoff: persisted for convenience across PSI + Site Audit flows.
|
// Same tradeoff: persisted for convenience across PSI + Site Audit flows.
|
||||||
@ -172,7 +170,7 @@ export const saveProjectPsiApiKey = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const clearProjectPsiApiKey = createServerFn({ method: "POST" })
|
export const clearProjectPsiApiKey = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiProjectSchema.parse(data))
|
.inputValidator((data: unknown) => psiProjectSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
await KeywordResearchRepository.clearProjectPsiApiKey(
|
await KeywordResearchRepository.clearProjectPsiApiKey(
|
||||||
@ -183,7 +181,7 @@ export const clearProjectPsiApiKey = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const listProjectPsiAudits = createServerFn({ method: "POST" })
|
export const listProjectPsiAudits = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiAuditListSchema.parse(data))
|
.inputValidator((data: unknown) => psiAuditListSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const rows = await PsiAuditRepository.listAuditResults({
|
const rows = await PsiAuditRepository.listAuditResults({
|
||||||
@ -219,7 +217,7 @@ export const listProjectPsiAudits = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getProjectPsiAuditRaw = createServerFn({ method: "POST" })
|
export const getProjectPsiAuditRaw = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiAuditDetailsSchema.parse(data))
|
.inputValidator((data: unknown) => psiAuditDetailsSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const row = await PsiAuditRepository.getAuditResult({
|
const row = await PsiAuditRepository.getAuditResult({
|
||||||
@ -229,11 +227,11 @@ export const getProjectPsiAuditRaw = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error("Audit not found");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row.r2Key) {
|
if (!row.r2Key) {
|
||||||
throw new Error("Audit payload not available");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadJson = await getJsonFromR2(row.r2Key);
|
const payloadJson = await getJsonFromR2(row.r2Key);
|
||||||
@ -247,7 +245,7 @@ export const getProjectPsiAuditRaw = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getProjectPsiAuditIssues = createServerFn({ method: "POST" })
|
export const getProjectPsiAuditIssues = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiIssueFilterSchema.parse(data))
|
.inputValidator((data: unknown) => psiIssueFilterSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const row = await PsiAuditRepository.getAuditResult({
|
const row = await PsiAuditRepository.getAuditResult({
|
||||||
@ -257,11 +255,11 @@ export const getProjectPsiAuditIssues = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error("Audit not found");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row.r2Key) {
|
if (!row.r2Key) {
|
||||||
throw new Error("Audit payload not available");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadJson = await getJsonFromR2(row.r2Key);
|
const payloadJson = await getJsonFromR2(row.r2Key);
|
||||||
@ -277,7 +275,7 @@ export const getProjectPsiAuditIssues = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const exportProjectPsiAudit = createServerFn({ method: "POST" })
|
export const exportProjectPsiAudit = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiExportSchema.parse(data))
|
.inputValidator((data: unknown) => psiExportSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const row = await PsiAuditRepository.getAuditResult({
|
const row = await PsiAuditRepository.getAuditResult({
|
||||||
@ -287,11 +285,11 @@ export const exportProjectPsiAudit = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error("Audit not found");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!row.r2Key) {
|
if (!row.r2Key) {
|
||||||
throw new Error("Audit payload not available");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadJson = await getJsonFromR2(row.r2Key);
|
const payloadJson = await getJsonFromR2(row.r2Key);
|
||||||
@ -329,7 +327,7 @@ export const exportProjectPsiAudit = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getPsiIssuesBySource = createServerFn({ method: "POST" })
|
export const getPsiIssuesBySource = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiUnifiedIssueSchema.parse(data))
|
.inputValidator((data: unknown) => psiUnifiedIssueSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const target = await resolvePsiSource({
|
const target = await resolvePsiSource({
|
||||||
@ -340,7 +338,7 @@ export const getPsiIssuesBySource = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!target.r2Key) {
|
if (!target.r2Key) {
|
||||||
throw new Error("Audit payload not available");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadJson = await getJsonFromR2(target.r2Key);
|
const payloadJson = await getJsonFromR2(target.r2Key);
|
||||||
@ -356,7 +354,7 @@ export const getPsiIssuesBySource = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const exportPsiBySource = createServerFn({ method: "POST" })
|
export const exportPsiBySource = createServerFn({ method: "POST" })
|
||||||
.middleware([useSessionTokenClientMiddleware, ensureUserMiddleware])
|
.middleware(authenticatedServerFunctionMiddleware)
|
||||||
.inputValidator((data: unknown) => psiUnifiedExportSchema.parse(data))
|
.inputValidator((data: unknown) => psiUnifiedExportSchema.parse(data))
|
||||||
.handler(async ({ data, context }) => {
|
.handler(async ({ data, context }) => {
|
||||||
const target = await resolvePsiSource({
|
const target = await resolvePsiSource({
|
||||||
@ -367,7 +365,7 @@ export const exportPsiBySource = createServerFn({ method: "POST" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!target.r2Key) {
|
if (!target.r2Key) {
|
||||||
throw new Error("Audit payload not available");
|
throw new AppError("NOT_FOUND");
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadJson = await getJsonFromR2(target.r2Key);
|
const payloadJson = await getJsonFromR2(target.r2Key);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user