From 3511a7650840c3da86a927dc0b451a0eb9955c1d Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Wed, 4 Mar 2026 13:58:15 -0500 Subject: [PATCH] Improve fresh install flow and auth page routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move /setup out of protected (pages) route group so it's accessible without auth (fixes fresh install redirect loop) - Redirect /login → /register when zero users exist - Add "Get Started" button on landing page for fresh installs - Hide register button/link when registration is closed - Add auth redirects: logged-in users on /login or /register → /dashboard - Convert register page to server component with server-side checks - Fix animation snap on auth form buttons (transition-all → scoped) - Update proxy middleware with /login and /register routes Co-Authored-By: Claude Opus 4.6 --- app/(auth)/login/page.tsx | 14 +++++- app/(auth)/register/page.tsx | 70 +++++++++----------------- app/{(pages) => (auth)}/setup/page.tsx | 0 app/page.tsx | 14 +++++- components/auth-form.tsx | 56 +++++++++++---------- components/landing-page.tsx | 50 ++++++++++++------ proxy.ts | 12 ++++- 7 files changed, 126 insertions(+), 90 deletions(-) rename app/{(pages) => (auth)}/setup/page.tsx (100%) diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index fa1d48e..cf0d831 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,11 +1,22 @@ +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; import { AuthForm } from "@/components/auth-form"; +import { auth } from "@/lib/auth/server"; import { getOidcProviderName, isOidcConfigured, isPasswordLoginDisabled, } from "@/lib/config"; +import { getUserCount, isRegistrationOpen } from "@/lib/services/settings"; + +export default async function LoginPage() { + const session = await auth.api.getSession({ headers: await headers() }); + if (session) redirect("/dashboard"); + + if (getUserCount() === 0) { + redirect("/register"); + } -export default function LoginPage() { const oidcEnabled = isOidcConfigured(); return ( @@ -16,6 +27,7 @@ export default function LoginPage() { oidcEnabled, oidcProviderName: oidcEnabled ? getOidcProviderName() : null, passwordLoginDisabled: isPasswordLoginDisabled(), + registrationOpen: isRegistrationOpen(), }} /> diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index f9b1621..7084a17 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,55 +1,26 @@ -"use client"; - import { IconLock } from "@tabler/icons-react"; -import { motion } from "motion/react"; +import { headers } from "next/headers"; import Link from "next/link"; -import { useEffect, useState } from "react"; -import type { AuthConfig } from "@/components/auth-form"; +import { redirect } from "next/navigation"; import { AuthForm } from "@/components/auth-form"; +import { auth } from "@/lib/auth/server"; +import { + getOidcProviderName, + isOidcConfigured, + isPasswordLoginDisabled, +} from "@/lib/config"; +import { isRegistrationOpen } from "@/lib/services/settings"; -export default function RegisterPage() { - const [registrationOpen, setRegistrationOpen] = useState( - null, - ); - const [authConfig, setAuthConfig] = useState({ - oidcEnabled: false, - oidcProviderName: null, - passwordLoginDisabled: false, - }); +export default async function RegisterPage() { + const session = await auth.api.getSession({ headers: await headers() }); + if (session) redirect("/dashboard"); - useEffect(() => { - fetch("/api/registration/status") - .then((res) => res.json()) - .then((data) => { - setRegistrationOpen(data.registrationOpen); - setAuthConfig({ - oidcEnabled: data.oidcEnabled ?? false, - oidcProviderName: data.oidcProviderName ?? null, - passwordLoginDisabled: data.passwordLoginDisabled ?? false, - }); - }) - .catch(() => setRegistrationOpen(false)); - }, []); - - if (registrationOpen === null) { - return
; - } - - if (!registrationOpen) { + if (!isRegistrationOpen()) { return (
- +
@@ -68,15 +39,24 @@ export default function RegisterPage() { > Sign in instead - +
); } + const oidcEnabled = isOidcConfigured(); + return (
- +
); } diff --git a/app/(pages)/setup/page.tsx b/app/(auth)/setup/page.tsx similarity index 100% rename from app/(pages)/setup/page.tsx rename to app/(auth)/setup/page.tsx diff --git a/app/page.tsx b/app/page.tsx index d587d47..ad3e010 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,6 @@ +import { connection } from "next/server"; import { LandingPage } from "@/components/landing-page"; +import { getUserCount, isRegistrationOpen } from "@/lib/services/settings"; import { tmdbImageUrl } from "@/lib/tmdb/image"; // Well-known TMDB poster paths for the background collage @@ -21,6 +23,14 @@ const posterUrls = posterPaths .map((p) => tmdbImageUrl(p, "w300")) .filter(Boolean) as string[]; -export default function Home() { - return ; +export default async function Home() { + await connection(); + const userCount = getUserCount(); + return ( + + ); } diff --git a/components/auth-form.tsx b/components/auth-form.tsx index 240a2a6..eee85b1 100644 --- a/components/auth-form.tsx +++ b/components/auth-form.tsx @@ -12,6 +12,7 @@ export interface AuthConfig { oidcEnabled: boolean; oidcProviderName: string | null; passwordLoginDisabled: boolean; + registrationOpen?: boolean; } const fieldVariants = { @@ -122,7 +123,7 @@ export function AuthForm({ disabled={oidcLoading} variants={fieldVariants} whileTap={{ scale: 0.98 }} - className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-border/50 bg-background/50 text-sm font-medium transition-all hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50" + className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-border/50 bg-background/50 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50" > {oidcLoading @@ -213,7 +214,7 @@ export function AuthForm({ disabled={loading} variants={fieldVariants} whileTap={{ scale: 0.98 }} - className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-primary font-medium text-primary-foreground transition-all hover:shadow-lg hover:shadow-primary/20 disabled:pointer-events-none disabled:opacity-50" + className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-primary font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20 disabled:pointer-events-none disabled:opacity-50" > {loading ? "Loading..." @@ -237,31 +238,32 @@ export function AuthForm({ )} - {showPasswordForm && ( -

- {isRegister ? ( - <> - Already have an account?{" "} - - Sign in - - - ) : ( - <> - Don't have an account?{" "} - - Register - - - )} -

- )} + {showPasswordForm && + (isRegister || authConfig?.registrationOpen !== false) && ( +

+ {isRegister ? ( + <> + Already have an account?{" "} + + Sign in + + + ) : ( + <> + Don't have an account?{" "} + + Register + + + )} +

+ )}
); diff --git a/components/landing-page.tsx b/components/landing-page.tsx index b5917d9..5901eed 100644 --- a/components/landing-page.tsx +++ b/components/landing-page.tsx @@ -40,7 +40,15 @@ const posterLayout = [ { x: "78%", y: "66%", rotate: 5, delay: 0.23 }, ]; -export function LandingPage({ posterUrls }: { posterUrls: string[] }) { +export function LandingPage({ + posterUrls, + freshInstall, + registrationOpen, +}: { + posterUrls: string[]; + freshInstall: boolean; + registrationOpen: boolean; +}) { const { data: session, isPending } = useSession(); const router = useRouter(); @@ -169,19 +177,33 @@ export function LandingPage({ posterUrls }: { posterUrls: string[] }) { delay: 0.35, }} > - - Sign In -
- - - Register - + {freshInstall ? ( + + Get Started +
+ + ) : ( + <> + + Sign In +
+ + {registrationOpen && ( + + Register + + )} + + )} diff --git a/proxy.ts b/proxy.ts index f208e8f..a2c5cdd 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,6 +1,8 @@ import { getSessionCookie } from "better-auth/cookies"; import { type NextRequest, NextResponse } from "next/server"; +const authRoutes = new Set(["/login", "/register"]); + export function proxy(request: NextRequest) { const sessionCookie = getSessionCookie(request); const { pathname } = request.nextUrl; @@ -12,7 +14,13 @@ export function proxy(request: NextRequest) { return NextResponse.next(); } - if (!sessionCookie) { + // Logged-in users on auth pages → dashboard + if (authRoutes.has(pathname) && sessionCookie) { + return NextResponse.redirect(new URL("/dashboard", request.url)); + } + + // Unauthenticated users on protected pages → login + if (!authRoutes.has(pathname) && !sessionCookie) { return NextResponse.redirect(new URL("/login", request.url)); } @@ -22,6 +30,8 @@ export function proxy(request: NextRequest) { export const config = { matcher: [ "/", + "/login", + "/register", "/dashboard", "/explore", "/settings",