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

+ +`; +}