diff --git a/.env.example b/.env.example index 75cc9515..67135dff 100644 --- a/.env.example +++ b/.env.example @@ -3,12 +3,6 @@ # Format: postgres://[user]:[password]@[host]:[port]/[database]?[options] DATABASE_URL=postgres://postgres:password@localhost:5432/byos_db?sslmode=disable -# Public Base URL (Optional) -# Absolute URL where this instance is reachable, e.g. https://byos.example.com -# Used for absolute metadata/Open Graph URLs and the browser renderer. -# Defaults to http://localhost:3000 when unset. -NEXT_PUBLIC_BASE_URL= - # React Renderer Configuration # Options: "takumi" (default) or "satori" or "browser" # Controls which rendering engine is used for screen generation @@ -34,6 +28,10 @@ FORCE_WIKIPEDIA_RESERVOIR=false # Set to "false" to disable authentication (mono-user mode) AUTH_ENABLED=true BETTER_AUTH_SECRET=your_better_auth_secret_32_characters_min +# Public URL of this instance, used by better-auth (origin checks, email links) +# and for absolute metadata/Open Graph URLs. Auto-detected on Vercel; otherwise +# defaults to http://localhost:3000. +BETTER_AUTH_URL= # The first account created during setup becomes admin automatically. # TRMNL Live Proxy (Optional) diff --git a/app/(app)/device/[friendly_id]/client-page.tsx b/app/(app)/device/[friendly_id]/client-page.tsx index aa66c33c..f2fb9954 100644 --- a/app/(app)/device/[friendly_id]/client-page.tsx +++ b/app/(app)/device/[friendly_id]/client-page.tsx @@ -260,7 +260,7 @@ export default function DeviceClientPage({ }; // Handle form submission - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); // Validate API key diff --git a/app/(auth)/recover/page.tsx b/app/(auth)/recover/page.tsx index d756d3bc..ad14b2cc 100644 --- a/app/(auth)/recover/page.tsx +++ b/app/(auth)/recover/page.tsx @@ -45,7 +45,7 @@ function RecoverPageContent() { } }, [error]); - const handleRequestReset = async (e: React.FormEvent) => { + const handleRequestReset = async (e: React.SubmitEvent) => { e.preventDefault(); setErrorMessage(""); setSuccessMessage(""); @@ -77,7 +77,7 @@ function RecoverPageContent() { } }; - const handleResetPassword = async (e: React.FormEvent) => { + const handleResetPassword = async (e: React.SubmitEvent) => { e.preventDefault(); setErrorMessage(""); setSuccessMessage(""); diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx index 777e187e..2cd3308a 100644 --- a/app/(auth)/sign-in/page.tsx +++ b/app/(auth)/sign-in/page.tsx @@ -1,4 +1,5 @@ import { redirect } from "next/navigation"; +import { connection } from "next/server"; import { Suspense } from "react"; import { getDatabaseSetupStatus } from "@/lib/database/utils"; import SignInForm from "./sign-in-form"; @@ -6,6 +7,7 @@ import SignInForm from "./sign-in-form"; // Next.js Cache Components require that uncached data fetches live inside a // boundary, so the DB probe is isolated to a child component. async function SignInWithDbCheck() { + await connection(); const setup = await getDatabaseSetupStatus(); if (setup.needsSetup) { redirect("/setup"); diff --git a/app/(auth)/sign-in/sign-in-form.tsx b/app/(auth)/sign-in/sign-in-form.tsx index da74c87a..c03d9e67 100644 --- a/app/(auth)/sign-in/sign-in-form.tsx +++ b/app/(auth)/sign-in/sign-in-form.tsx @@ -1,7 +1,6 @@ "use client"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -21,13 +20,12 @@ interface SignInFormProps { } export default function SignInForm({ dbReady, dbError }: SignInFormProps) { - const router = useRouter(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); if (!dbReady) return; setError(""); @@ -46,8 +44,7 @@ export default function SignInForm({ dbReady, dbError }: SignInFormProps) { } if (data) { - router.push("/"); - router.refresh(); + window.location.href = "/"; } } catch (_err) { setError("An unexpected error occurred. Please try again."); diff --git a/app/(auth)/sign-up/sign-up-form.tsx b/app/(auth)/sign-up/sign-up-form.tsx index d83dd795..edfe10e7 100644 --- a/app/(auth)/sign-up/sign-up-form.tsx +++ b/app/(auth)/sign-up/sign-up-form.tsx @@ -1,7 +1,6 @@ "use client"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -16,7 +15,6 @@ import { Label } from "@/components/ui/label"; import { authClient } from "@/lib/auth/auth-client"; export default function SignUpForm() { - const router = useRouter(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -24,7 +22,7 @@ export default function SignUpForm() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); - const handleSubmit = async (e: React.FormEvent) => { + const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); setError(""); @@ -56,9 +54,7 @@ export default function SignUpForm() { } if (data) { - // Redirect to home page on successful sign up - router.push("/"); - router.refresh(); + window.location.href = "/"; } } catch (_err) { setError("An unexpected error occurred. Please try again."); diff --git a/app/layout.tsx b/app/layout.tsx index ca96fcc3..e491fd7b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,31 +4,19 @@ import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { getAllFontVariables } from "@/lib/fonts"; -import { cn } from "@/lib/utils"; +import { cn, getAppBaseUrl } from "@/lib/utils"; const META_THEME_COLORS = { light: "#ffffff", dark: "#09090b", }; -function getMetadataBase(): URL { - const raw = process.env.NEXT_PUBLIC_BASE_URL?.trim(); - if (raw) { - try { - return new URL(raw); - } catch { - // fall through to the default below - } - } - return new URL("http://localhost:3000"); -} - const APP_NAME = "TRMNL BYOS"; const APP_DESCRIPTION = "Self-hosted server and device management dashboard for TRMNL e-ink displays."; export const metadata: Metadata = { - metadataBase: getMetadataBase(), + metadataBase: getAppBaseUrl(), title: { default: APP_NAME, template: `%s ยท ${APP_NAME}`, diff --git a/components/device/device-edit-form.tsx b/components/device/device-edit-form.tsx index aceac6b8..c066f825 100644 --- a/components/device/device-edit-form.tsx +++ b/components/device/device-edit-form.tsx @@ -75,7 +75,7 @@ interface DeviceEditFormProps { onRegenerateFriendlyId: () => void; onAddTimeRange: () => void; onRemoveTimeRange: (index: number) => void; - onSubmit: (e: React.FormEvent) => void; + onSubmit: (e: React.SubmitEvent) => void; onCancel: () => void; } diff --git a/components/nav-user.tsx b/components/nav-user.tsx index 6ce7fbe9..04a04aee 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -10,7 +10,6 @@ import { } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, @@ -41,7 +40,6 @@ interface NavUserProps { export function NavUser({ user }: NavUserProps) { const { isMobile } = useSidebar(); - const router = useRouter(); const getUserInitials = () => { if (user.name) { @@ -59,8 +57,7 @@ export function NavUser({ user }: NavUserProps) { const handleSignOut = async () => { await authClient.signOut(); - router.push("/sign-in"); - router.refresh(); + window.location.replace("/sign-in"); }; return ( diff --git a/components/recipes/screen-params-form.tsx b/components/recipes/screen-params-form.tsx index 7725ec18..1e0b74ce 100644 --- a/components/recipes/screen-params-form.tsx +++ b/components/recipes/screen-params-form.tsx @@ -132,7 +132,7 @@ export function ScreenParamsForm({ [values, initial], ); - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = (event: React.SubmitEvent) => { event.preventDefault(); setFormStatus("idle"); setStatusMessage(""); diff --git a/lib/auth/auth.ts b/lib/auth/auth.ts index 6316c9ae..6f2ba898 100644 --- a/lib/auth/auth.ts +++ b/lib/auth/auth.ts @@ -2,6 +2,7 @@ import { betterAuth } from "better-auth"; import { admin } from "better-auth/plugins"; import { Pool } from "pg"; import { sendEmail } from "@/lib/email"; +import { getAppBaseUrl } from "../utils"; const AUTH_ENABLED = process.env.AUTH_ENABLED !== "false"; const BYOS_MONO_USER_ID = "byos_mono_user"; @@ -52,6 +53,7 @@ function createAuth() { } return betterAuth({ + baseURL: getAppBaseUrl().origin, database: pool, emailAndPassword: { enabled: true, diff --git a/lib/recipes/renderers/browser.ts b/lib/recipes/renderers/browser.ts index c0be392e..43f1448d 100644 --- a/lib/recipes/renderers/browser.ts +++ b/lib/recipes/renderers/browser.ts @@ -53,8 +53,7 @@ export async function renderWithBrowser( options: RenderWithBrowserOptions = {}, ): Promise { const port = process.env.PORT || 3000; - const baseUrl = - process.env.NEXT_PUBLIC_BASE_URL ?? `http://127.0.0.1:${port}`; + const baseUrl = `http://127.0.0.1:${port}`; const params = new URLSearchParams({ width: String(width), height: String(height), diff --git a/lib/utils.ts b/lib/utils.ts index ac680b30..64a2a876 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -4,3 +4,21 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function getAppBaseUrl(): URL { + const candidates: Array = [ + process.env.BETTER_AUTH_URL, + process.env.VERCEL_PROJECT_PRODUCTION_URL && + `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`, + process.env.VERCEL_URL && `https://${process.env.VERCEL_URL}`, + ]; + + for (const candidate of candidates) { + const raw = candidate?.trim(); + if (raw && URL.canParse(raw)) { + return new URL(raw); + } + } + + return new URL("http://localhost:3000"); +}