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 { cache } from "react"; import { getDb } from "@/db"; import { accounts, sessions, users, verifications, } from "@/db/schema"; import { isAuthorizedAdmin, type SessionUserLike } from "./authorization"; import { HttpError } from "@/lib/security/http"; import { consumeRateLimit } from "@/lib/security/rate-limit"; 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 Guide", 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: false, minPasswordLength: 6, maxPasswordLength: 128, }, advanced: { ipAddress: { ipAddressHeaders: process.env.TRUSTED_CLIENT_IP_HEADER ? [process.env.TRUSTED_CLIENT_IP_HEADER] : [], }, }, rateLimit: { enabled: true, customStorage: { consume: (key, rule) => consumeRateLimit(`auth:${key}`, rule) }, customRules: { "/sign-in/email": { window: 60, max: 10 } }, }, plugins: [ ...(process.env.NODE_ENV === "development" ? [] : [ captcha({ provider: "cloudflare-turnstile", secretKey: required("TURNSTILE_SECRET_KEY"), endpoints: ["/sign-in/email", "/sign-up/email"], }), ]), admin({ defaultRole: "user" }), 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 const getAdminSession = cache(async (): Promise => { // 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 HttpError { constructor() { super(401, "unauthorized"); this.name = "AdminAuthorizationError"; } } export async function requireAdmin(): Promise { const session = await getAdminSession(); if (!session) throw new AdminAuthorizationError(); return session; } export const getCustomerSession = cache(async () => { if (!hasAuthConfiguration()) return null; const session = await getAuth().api.getSession({ headers: await headers() }); if (!session?.session) return null; return { user: session.user, session: { id: session.session.id } }; });