import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { admin, organization } from "better-auth/plugins"; import { nextCookies } from "better-auth/next-js"; import { prisma } from "@/lib/db"; import { sendEmail, emailLayout } from "@/lib/email"; const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; const googleConfigured = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); // Fail fast in production if the signing secret is missing — sessions/cookies are // only secure when BETTER_AUTH_SECRET is set. Stay frictionless in dev/test. if (!process.env.BETTER_AUTH_SECRET && process.env.NODE_ENV === "production") { throw new Error("BETTER_AUTH_SECRET must be set in production."); } export const auth = betterAuth({ appName: "PodcastYes", secret: process.env.BETTER_AUTH_SECRET, baseURL: process.env.BETTER_AUTH_URL ?? appUrl, database: prismaAdapter(prisma, { provider: "postgresql" }), // Built-in brute-force protection for auth endpoints (login, password reset, etc). // 30 requests per 60s window per IP. rateLimit: { enabled: true, window: 60, max: 30 }, emailAndPassword: { enabled: true, // SECURITY GATE (currently OPEN): unverified emails can sign in. Flip this to // `true` once email delivery is confirmed working in prod. Left `false` for now // so existing dev accounts aren't locked out. Verification emails ARE sent on // signup (see emailVerification.sendOnSignUp below), so users can verify already. requireEmailVerification: false, minPasswordLength: 8, async sendResetPassword({ user, url }) { await sendEmail({ to: user.email, subject: "Reset your PodcastYes password", html: emailLayout( "Reset your password", "Click the button below to choose a new password.", { label: "Reset password", url } ), text: `Reset your password: ${url}`, }); }, }, emailVerification: { sendOnSignUp: true, async sendVerificationEmail({ user, url }) { await sendEmail({ to: user.email, subject: "Verify your email for PodcastYes", html: emailLayout( "Confirm your email", "Confirm your email address to secure your account.", { label: "Verify email", url } ), text: `Verify your email: ${url}`, }); }, }, socialProviders: googleConfigured ? { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, } : undefined, session: { expiresIn: 60 * 60 * 24 * 30, // 30 days updateAge: 60 * 60 * 24, // refresh daily // Cache the session in a signed cookie to avoid a DB hit on every request. // Tradeoff: a banned/demoted user keeps cached access until the cache expires, // so we keep the window short (60s). Shorter = faster revocation but more DB hits. cookieCache: { enabled: true, maxAge: 60 }, }, account: { accountLinking: { enabled: true, trustedProviders: ["google"] }, }, plugins: [ admin({ defaultRole: "user", adminRoles: ["admin"] }), organization({ teams: { enabled: true, maximumTeams: 1 }, // Agency seat cap is enforced in app logic against the subscription's seat count. membershipLimit: 5, }), // Must remain last: lets Server Actions / route handlers set auth cookies. nextCookies(), ], }); export type Session = typeof auth.$Infer.Session;