import "server-only"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; import { admin, captcha } from "better-auth/plugins"; import { headers } from "next/headers"; import { getDb } from "@/db"; import { accounts, sessions, users, verifications, } from "@/db/schema"; import { isAuthorizedAdmin, type SessionUserLike } from "./authorization"; function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(`${name} is required for authentication.`); return value; } function hasAuthConfiguration(): boolean { return ["DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET"].every( (name) => Boolean(process.env[name]), ); } function createAuth() { return betterAuth({ appName: "Buzz Sheet", database: drizzleAdapter(getDb(), { provider: "pg", schema: { user: users, session: sessions, account: accounts, verification: verifications, }, transaction: true, }), baseURL: required("BETTER_AUTH_URL"), trustedOrigins: process.env.BETTER_AUTH_TRUSTED_ORIGINS ?.split(",") .map((origin) => origin.trim()) .filter(Boolean), secret: required("BETTER_AUTH_SECRET"), emailAndPassword: { enabled: true, disableSignUp: true, minPasswordLength: 8, maxPasswordLength: 128, }, databaseHooks: { user: { create: { before: async () => ({ data: { emailVerified: true } }), }, }, }, plugins: [ ...(process.env.NODE_ENV === "development" ? [] : [ captcha({ provider: "cloudflare-turnstile", secretKey: required("TURNSTILE_SECRET_KEY"), endpoints: ["/sign-in/email"], }), ]), admin({ defaultRole: "admin" }), nextCookies(), ], }); } type AuthInstance = ReturnType; let authInstance: AuthInstance | undefined; export function getAuth(): AuthInstance { if (!authInstance) authInstance = createAuth(); return authInstance; } export interface AdminSession { user: SessionUserLike; session: { id: string }; } export async function getAdminSession(): Promise { if (process.env.BUZZ_DEMO_MODE === "true") { return { user: { id: "demo-admin", email: "demo@buzz-sheet.local", emailVerified: true, name: "Demo Admin", }, session: { id: "demo-session" }, }; } // Public pages can be prerendered without the runtime auth secret. In that // case the header simply omits the admin link; auth routes still fail loudly // through getAuth() when authentication is actually used. if (!hasAuthConfiguration()) return null; const session = await getAuth().api.getSession({ headers: await headers() }); if ( !session?.session || !isAuthorizedAdmin(session.user) ) { return null; } return { user: session.user, session: { id: session.session.id }, }; } export class AdminAuthorizationError extends Error { constructor() { super("Admin authorization required."); this.name = "AdminAuthorizationError"; } } export async function requireAdmin(): Promise { const session = await getAdminSession(); if (!session) throw new AdminAuthorizationError(); return session; }