import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { eq } from "drizzle-orm"; import bcrypt from "bcrypt"; import { db } from "~/server/db"; import * as schema from "~/database/schema"; import { generateFlagConfig } from "~/lib/flag-generator"; import { sendEmail, wrapInEmailTemplate, emailButton, emailParagraph } from "~/lib/email.server"; if (!process.env.BETTER_AUTH_SECRET) { throw new Error("BETTER_AUTH_SECRET is required"); } const googleProvider = process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET ? { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET } } : {}; const discordProvider = process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET ? { discord: { clientId: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET } } : {}; const appBaseUrl = process.env.APP_URL ?? process.env.BETTER_AUTH_URL; export const auth = betterAuth({ baseURL: appBaseUrl, database: drizzleAdapter(db, { provider: "pg", usePlural: true, }), databaseHooks: { user: { create: { before: async (data) => { return { data: { ...data, flagConfig: generateFlagConfig(data.id), } as typeof data, }; }, }, update: { before: async (data) => { const incomingImage = "imageUrl" in data ? data.imageUrl : "image" in data ? data.image : undefined; const userId = typeof data.id === "string" ? data.id : null; if (!incomingImage || !userId) return; const existing = await db.query.users.findFirst({ where: eq(schema.users.id, userId), }); if (existing?.avatarType === "uploaded" || existing?.avatarType === "flag") { const next = { ...data }; delete next.imageUrl; delete next.image; return { data: next }; } }, }, }, }, advanced: { database: { generateId: () => crypto.randomUUID(), }, }, user: { fields: { name: "displayName", image: "imageUrl", }, additionalFields: { username: { type: "string", required: false, fieldName: "username" }, isAdmin: { type: "boolean", defaultValue: false, fieldName: "isAdmin" }, }, }, emailAndPassword: { enabled: true, password: { hash: (password: string) => bcrypt.hash(password, 10), verify: ({ hash, password }: { hash: string; password: string }) => bcrypt.compare(password, hash), }, sendResetPassword: async ({ user, url }) => { const { error } = await sendEmail({ to: user.email, subject: "Reset your Brackt password", html: wrapInEmailTemplate( emailParagraph("Click the button below to reset your Brackt password. This link expires in 1 hour.") + emailButton(url, "Reset My Password") + emailParagraph(`Or copy this link: ${url}`), "Reset your Brackt password — link expires in 1 hour." ), }); if (error) throw error; }, }, emailVerification: { sendOnSignUp: true, requireEmailVerification: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { const { error } = await sendEmail({ to: user.email, subject: "Verify your Brackt email", html: wrapInEmailTemplate( emailParagraph("Thanks for signing up for Brackt! Click the button below to verify your email address. This link expires in 24 hours.") + emailButton(url, "Verify My Email") + emailParagraph(`Or copy this link: ${url}`), "Verify your email to start using Brackt." ), }); if (error) throw error; }, }, socialProviders: { ...googleProvider, ...discordProvider, }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "discord"], }, }, });