From 399dbec38d45c00cc22b9237ef547a0b7e826c55 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 22 May 2026 14:39:31 +0000 Subject: [PATCH] fix(social): clear error page when an OAuth provider isn't configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: /api/sp/oauth/threads/start returns a generic 500. Root cause: META_APP_ID / META_APP_SECRET (and every other social provider's credentials) are not set on Railway, so the platform module throws "META_APP_ID not configured" deep in the call stack. Hardened each /start handler. New helper lib/sp/require-env.ts maps platform name → required env keys. Every start route calls requirePlatformEnv(platform) first; if any key is missing it returns a 503 with a human-readable HTML page that tells the user the integration isn't set up and lists the missing env vars for an admin to copy into Railway. Updated routes: - /api/sp/oauth/threads/start (META_APP_ID, META_APP_SECRET) - /api/sp/oauth/facebook/start (META_APP_ID, META_APP_SECRET) - /api/sp/oauth/linkedin/start (LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET) - /api/sp/oauth/x/start (X_CLIENT_ID, X_CLIENT_SECRET) - /api/sp/oauth/reddit/start (REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET) Mastodon /start is per-instance OAuth (no env-level credentials) so it's untouched. Code-only fix — actually making the integrations WORK still needs the admin to register the OAuth apps on each platform and set the credentials on Railway. The error page now tells them exactly which keys are missing. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/sp/oauth/facebook/start/route.ts | 4 + app/api/sp/oauth/linkedin/start/route.ts | 4 + app/api/sp/oauth/reddit/start/route.ts | 4 + app/api/sp/oauth/threads/start/route.ts | 4 + app/api/sp/oauth/x/start/route.ts | 4 + lib/sp/require-env.ts | 108 +++++++++++++++++++++++ 6 files changed, 128 insertions(+) create mode 100644 lib/sp/require-env.ts diff --git a/app/api/sp/oauth/facebook/start/route.ts b/app/api/sp/oauth/facebook/start/route.ts index 3e551a21..6bfae122 100644 --- a/app/api/sp/oauth/facebook/start/route.ts +++ b/app/api/sp/oauth/facebook/start/route.ts @@ -5,11 +5,15 @@ import crypto from "node:crypto"; import { createClient } from "@/lib/supabase/server"; import { env } from "@/lib/env"; import { getFacebookAuthorizeUrl } from "@/lib/sp/platforms/facebook"; +import { requirePlatformEnv } from "@/lib/sp/require-env"; const STATE_COOKIE = "sp_facebook_state"; const STATE_TTL_S = 600; export async function GET() { + const notConfigured = requirePlatformEnv("facebook"); + if (notConfigured) return notConfigured; + const supabase = await createClient(); const { data: { user }, diff --git a/app/api/sp/oauth/linkedin/start/route.ts b/app/api/sp/oauth/linkedin/start/route.ts index 08024e29..a12baf35 100644 --- a/app/api/sp/oauth/linkedin/start/route.ts +++ b/app/api/sp/oauth/linkedin/start/route.ts @@ -7,11 +7,15 @@ import crypto from "node:crypto"; import { createClient } from "@/lib/supabase/server"; import { env } from "@/lib/env"; import { getLinkedinAuthorizeUrl } from "@/lib/sp/platforms/linkedin"; +import { requirePlatformEnv } from "@/lib/sp/require-env"; const STATE_COOKIE = "sp_linkedin_state"; const STATE_TTL_S = 600; export async function GET() { + const notConfigured = requirePlatformEnv("linkedin"); + if (notConfigured) return notConfigured; + const supabase = await createClient(); const { data: { user }, diff --git a/app/api/sp/oauth/reddit/start/route.ts b/app/api/sp/oauth/reddit/start/route.ts index 3abcf102..bb5ddaf4 100644 --- a/app/api/sp/oauth/reddit/start/route.ts +++ b/app/api/sp/oauth/reddit/start/route.ts @@ -8,11 +8,15 @@ import crypto from "node:crypto"; import { createClient } from "@/lib/supabase/server"; import { env } from "@/lib/env"; import { getRedditAuthorizeUrl } from "@/lib/sp/platforms/reddit"; +import { requirePlatformEnv } from "@/lib/sp/require-env"; const STATE_COOKIE = "sp_reddit_state"; const STATE_TTL_S = 600; export async function GET() { + const notConfigured = requirePlatformEnv("reddit"); + if (notConfigured) return notConfigured; + const supabase = await createClient(); const { data: { user }, diff --git a/app/api/sp/oauth/threads/start/route.ts b/app/api/sp/oauth/threads/start/route.ts index b7d360ae..bf878bda 100644 --- a/app/api/sp/oauth/threads/start/route.ts +++ b/app/api/sp/oauth/threads/start/route.ts @@ -5,11 +5,15 @@ import crypto from "node:crypto"; import { createClient } from "@/lib/supabase/server"; import { env } from "@/lib/env"; import { getThreadsAuthorizeUrl } from "@/lib/sp/platforms/threads"; +import { requirePlatformEnv } from "@/lib/sp/require-env"; const STATE_COOKIE = "sp_threads_state"; const STATE_TTL_S = 600; export async function GET() { + const notConfigured = requirePlatformEnv("threads"); + if (notConfigured) return notConfigured; + const supabase = await createClient(); const { data: { user }, diff --git a/app/api/sp/oauth/x/start/route.ts b/app/api/sp/oauth/x/start/route.ts index ff82e3c7..2e4f5a53 100644 --- a/app/api/sp/oauth/x/start/route.ts +++ b/app/api/sp/oauth/x/start/route.ts @@ -8,11 +8,15 @@ import { createClient } from "@/lib/supabase/server"; import { env } from "@/lib/env"; import { generatePkcePair } from "@/lib/sp/pkce"; import { getXAuthorizeUrl } from "@/lib/sp/platforms/x"; +import { requirePlatformEnv } from "@/lib/sp/require-env"; const STATE_COOKIE = "sp_x_state"; const STATE_TTL_S = 600; export async function GET() { + const notConfigured = requirePlatformEnv("x"); + if (notConfigured) return notConfigured; + const supabase = await createClient(); const { data: { user }, diff --git a/lib/sp/require-env.ts b/lib/sp/require-env.ts new file mode 100644 index 00000000..64e37e4f --- /dev/null +++ b/lib/sp/require-env.ts @@ -0,0 +1,108 @@ +// Shared helper for the social OAuth /start routes. Each provider needs +// a small set of env vars (client id + secret). If any are missing, we +// return a clear HTML error page up front instead of letting the +// platform module throw deep in the call stack and surface a generic +// 500 to the user. + +import { NextResponse } from "next/server"; +import { env } from "@/lib/env"; + +interface PlatformConfig { + label: string; + /** Env keys required for the OAuth flow to function. */ + envKeys: string[]; + /** Env keys we read (i.e. values from lib/env.ts). */ + envValues: () => Array; +} + +const PLATFORMS: Record = { + threads: { + label: "Threads", + envKeys: ["META_APP_ID", "META_APP_SECRET"], + envValues: () => [env.metaAppId, env.metaAppSecret], + }, + facebook: { + label: "Facebook", + envKeys: ["META_APP_ID", "META_APP_SECRET"], + envValues: () => [env.metaAppId, env.metaAppSecret], + }, + linkedin: { + label: "LinkedIn", + envKeys: ["LINKEDIN_CLIENT_ID", "LINKEDIN_CLIENT_SECRET"], + envValues: () => [env.linkedinClientId, env.linkedinClientSecret], + }, + x: { + label: "X (Twitter)", + envKeys: ["X_CLIENT_ID", "X_CLIENT_SECRET"], + envValues: () => [env.xClientId, env.xClientSecret], + }, + reddit: { + label: "Reddit", + envKeys: ["REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET"], + envValues: () => [env.redditClientId, env.redditClientSecret], + }, +}; + +/** + * Returns null when the platform is fully configured. Otherwise returns + * a NextResponse with a 503 + a human-readable error page that tells + * the user (and an admin reading the page) exactly which env vars are + * missing and where to set them. + */ +export function requirePlatformEnv(platform: string): NextResponse | null { + const cfg = PLATFORMS[platform]; + if (!cfg) return null; // Unknown platform — let downstream handle it. + const values = cfg.envValues(); + const missing = cfg.envKeys.filter((_, i) => !values[i]); + if (missing.length === 0) return null; + + const html = renderNotConfiguredPage({ + label: cfg.label, + missing, + }); + return new NextResponse(html, { + status: 503, + headers: { "content-type": "text/html; charset=utf-8" }, + }); +} + +function renderNotConfiguredPage(input: { + label: string; + missing: string[]; +}): string { + return ` + + + + ${input.label} OAuth not configured · CrawlProof + + + + +

${input.label} OAuth isn't set up yet

+

CrawlProof can't start a ${input.label} OAuth flow because the + server is missing credentials. You'll need an admin to add them and + redeploy.

+ +

Admin: add these to Railway

+
${input.missing.map((k) => `${k}=…`).join("\n")}
+

Register an app on the provider's developer + dashboard, paste the client id + secret into the Railway service + variables, redeploy.

+ +

← Back to dashboard

+ +`; +}