Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/device/[friendly_id]/client-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/(auth)/recover/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function RecoverPageContent() {
}
}, [error]);

const handleRequestReset = async (e: React.FormEvent) => {
const handleRequestReset = async (e: React.SubmitEvent) => {
e.preventDefault();
setErrorMessage("");
setSuccessMessage("");
Expand Down Expand Up @@ -77,7 +77,7 @@ function RecoverPageContent() {
}
};

const handleResetPassword = async (e: React.FormEvent) => {
const handleResetPassword = async (e: React.SubmitEvent) => {
e.preventDefault();
setErrorMessage("");
setSuccessMessage("");
Expand Down
2 changes: 2 additions & 0 deletions app/(auth)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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";

// Next.js Cache Components require that uncached data fetches live inside a
// <Suspense> 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");
Expand Down
7 changes: 2 additions & 5 deletions app/(auth)/sign-in/sign-in-form.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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("");
Expand All @@ -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.");
Expand Down
8 changes: 2 additions & 6 deletions app/(auth)/sign-up/sign-up-form.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,15 +15,14 @@ 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("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault();
setError("");

Expand Down Expand Up @@ -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.");
Expand Down
16 changes: 2 additions & 14 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
2 changes: 1 addition & 1 deletion components/device/device-edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
5 changes: 1 addition & 4 deletions components/nav-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -41,7 +40,6 @@ interface NavUserProps {

export function NavUser({ user }: NavUserProps) {
const { isMobile } = useSidebar();
const router = useRouter();

const getUserInitials = () => {
if (user.name) {
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion components/recipes/screen-params-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function ScreenParamsForm({
[values, initial],
);

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
setFormStatus("idle");
setStatusMessage("");
Expand Down
2 changes: 2 additions & 0 deletions lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -52,6 +53,7 @@ function createAuth() {
}

return betterAuth({
baseURL: getAppBaseUrl().origin,
database: pool,
emailAndPassword: {
enabled: true,
Expand Down
3 changes: 1 addition & 2 deletions lib/recipes/renderers/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ export async function renderWithBrowser(
options: RenderWithBrowserOptions = {},
): Promise<Buffer> {
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),
Expand Down
18 changes: 18 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> = [
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");
}
Loading