From 8fc4c110b67e1f03b7ddca91ec2ef14280a5d6a9 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Sun, 1 Mar 2026 13:30:31 -0500 Subject: [PATCH] Add setup page and graceful handling for missing TMDB API key Without TMDB_API_KEY the app silently fails on search and import. This adds a /setup page that guides admins through obtaining and configuring the key, redirects unconfigured visitors from the landing page, and returns clear error messages from the search API. Co-Authored-By: Claude Opus 4.6 --- app/(pages)/setup/page.tsx | 263 +++++++++++++++++++++++++++++ app/api/search/route.ts | 11 ++ app/api/setup/status/route.ts | 8 + app/page.tsx | 20 ++- components/search-autocomplete.tsx | 10 ++ lib/config.ts | 8 + 6 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 app/(pages)/setup/page.tsx create mode 100644 app/api/setup/status/route.ts create mode 100644 lib/config.ts diff --git a/app/(pages)/setup/page.tsx b/app/(pages)/setup/page.tsx new file mode 100644 index 0000000..09f852d --- /dev/null +++ b/app/(pages)/setup/page.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { + IconCheck, + IconCopy, + IconExternalLink, + IconKey, +} from "@tabler/icons-react"; +import { motion } from "motion/react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; + +const steps = [ + { + number: "1", + title: "Create a TMDB account", + description: ( + <> + Head to{" "} + + themoviedb.org + + {" "} + and sign up for a free account. + + ), + }, + { + number: "2", + title: "Request an API key", + description: ( + <> + Go to{" "} + + Settings → API + + {" "} + and request an API key. Choose “Developer” when asked. You + need the{" "} + + API Read Access Token + {" "} + (the long one starting with{" "} + eyJ... + ). + + ), + }, + { + number: "3", + title: "Add it to your environment", + description: "Set the TMDB_API_KEY environment variable and restart Sofa.", + }, +]; + +const envSnippets = [ + { + label: ".env file", + code: "TMDB_API_KEY=your_api_read_access_token_here", + }, + { + label: "Docker Compose", + code: `environment: + - TMDB_API_KEY=your_api_read_access_token_here`, + }, + { + label: "Docker run", + code: "docker run -e TMDB_API_KEY=your_token ...", + }, +]; + +const sectionVariants = { + hidden: { opacity: 0, y: 24 }, + visible: { + opacity: 1, + y: 0, + transition: { type: "spring" as const, stiffness: 200, damping: 24 }, + }, +}; + +export default function SetupPage() { + const router = useRouter(); + const [checking, setChecking] = useState(false); + const [configured, setConfigured] = useState(null); + const [copiedIdx, setCopiedIdx] = useState(null); + + const checkStatus = useCallback(async () => { + setChecking(true); + try { + const res = await fetch("/api/setup/status"); + if (res.ok) { + const data = await res.json(); + setConfigured(data.tmdbConfigured); + if (data.tmdbConfigured) { + // Key is now set — redirect to landing after a beat + setTimeout(() => router.push("/"), 1500); + } + } + } finally { + setChecking(false); + } + }, [router]); + + useEffect(() => { + checkStatus(); + }, [checkStatus]); + + function copySnippet(idx: number, code: string) { + navigator.clipboard.writeText(code); + setCopiedIdx(idx); + setTimeout(() => setCopiedIdx(null), 2000); + } + + return ( + + {/* Header */} + +
+ + Setup required +
+

+ Connect to TMDB +

+

+ Sofa uses{" "} + + The Movie Database + {" "} + for movie & TV metadata, posters, and streaming availability. + You'll need a free API key to get started. +

+
+ + {/* Steps */} + + {steps.map((step, i) => ( +
+
+ + {step.number} + +
+
+

{step.title}

+

+ {step.description} +

+ + {/* Show env snippets for step 3 */} + {i === 2 && ( +
+ {envSnippets.map((snippet, idx) => ( +
+
+ + {snippet.label} + + +
+
+                        {snippet.code}
+                      
+
+ ))} +
+ )} +
+
+ ))} +
+ + {/* Status check */} + +
+ {configured === true ? ( +
+
+ +
+
+

+ TMDB API key detected +

+

+ Redirecting you to Sofa... +

+
+
+ ) : ( +
+
+

+ After setting the key and restarting: +

+

+ Click the button to verify your configuration +

+
+ +
+ )} +
+
+
+ ); +} diff --git a/app/api/search/route.ts b/app/api/search/route.ts index cc37fc4..2178872 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -1,8 +1,19 @@ import { type NextRequest, NextResponse } from "next/server"; +import { isTmdbConfigured } from "@/lib/config"; import { searchMovies, searchMulti, searchTv } from "@/lib/tmdb/client"; import type { TmdbSearchResponse } from "@/lib/tmdb/types"; export async function GET(req: NextRequest) { + if (!isTmdbConfigured()) { + return NextResponse.json( + { + error: "TMDB API key is not configured. Visit /setup for instructions.", + code: "TMDB_NOT_CONFIGURED", + }, + { status: 503 }, + ); + } + const query = req.nextUrl.searchParams.get("query"); const type = req.nextUrl.searchParams.get("type"); diff --git a/app/api/setup/status/route.ts b/app/api/setup/status/route.ts new file mode 100644 index 0000000..44f1d29 --- /dev/null +++ b/app/api/setup/status/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from "next/server"; +import { isTmdbConfigured } from "@/lib/config"; + +export async function GET() { + return NextResponse.json({ + tmdbConfigured: isTmdbConfigured(), + }); +} diff --git a/app/page.tsx b/app/page.tsx index d40852d..287eb1d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,6 +7,18 @@ import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { useSession } from "@/lib/auth/client"; +/** Check whether the server has TMDB configured. */ +async function fetchSetupStatus(): Promise { + try { + const res = await fetch("/api/setup/status"); + if (!res.ok) return true; // assume configured on error + const data = await res.json(); + return !!data.tmdbConfigured; + } catch { + return true; + } +} + // Well-known TMDB poster paths for the background collage const posterPaths = [ "/1E5baAaEse26fej7uHcjOgEERB2.jpg", // The Dark Knight @@ -48,9 +60,15 @@ export default function Home() { const router = useRouter(); useEffect(() => { - if (!isPending && session?.user) { + if (isPending) return; + if (session?.user) { router.replace("/dashboard"); + return; } + // If not logged in, check whether TMDB is configured + fetchSetupStatus().then((configured) => { + if (!configured) router.replace("/setup"); + }); }, [session, isPending, router]); if (isPending) return null; diff --git a/components/search-autocomplete.tsx b/components/search-autocomplete.tsx index 7394b67..f0a8d8c 100644 --- a/components/search-autocomplete.tsx +++ b/components/search-autocomplete.tsx @@ -52,6 +52,16 @@ export function SearchAutocomplete({ .then((r) => r.json()) .then((data) => { if (!cancelled) { + if (data.code === "TMDB_NOT_CONFIGURED") { + toast.error("TMDB API key not configured", { + description: "Visit /setup for instructions.", + action: { + label: "Setup", + onClick: () => (window.location.href = "/setup"), + }, + }); + return; + } const res = data.results ?? []; setResults(res); onResults?.(res); diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..1309f25 --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,8 @@ +/** + * Server-side configuration checks. + * Call these in route handlers or server components — never on the client. + */ + +export function isTmdbConfigured(): boolean { + return !!process.env.TMDB_API_KEY; +}