SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.7 KB · 139 lines typescript
Raw Blame History
1import "server-only";2import { betterAuth } from "better-auth";3import { drizzleAdapter } from "better-auth/adapters/drizzle";4import { nextCookies } from "better-auth/next-js";5import argon2 from "argon2";6import { getDb, users, sessions, accounts, verifications, userPreferences } from "@/db";7import { getEmailService } from "@/lib/email";8import { writeAudit } from "@/lib/audit";9import { APP_URL, IS_PROD } from "@/lib/env";10import { log } from "@/lib/log";1112const secret = process.env.AUTH_SECRET;13if ((!secret || secret.length < 32) && IS_PROD) {14  throw new Error("AUTH_SECRET must be set (32+ chars)");15}1617/** Argon2id — OWASP-recommended parameters (19 MiB, t=2, p=1). */18const ARGON2_OPTS = { type: argon2.argon2id, memoryCost: 19_456, timeCost: 2, parallelism: 1 } as const;1920export const auth = betterAuth({21  appName: "PolyLLM",22  baseURL: APP_URL,23  secret: secret ?? "dev-only-auth-secret-change-me-0123456789abcdef0123456789abcdef",24  trustedOrigins: [APP_URL, "http://localhost:3000"],25  database: drizzleAdapter(getDb(), {26    provider: "pg",27    usePlural: true,28    schema: { users, sessions, accounts, verifications },29  }),30  emailAndPassword: {31    enabled: true,32    minPasswordLength: 10,33    maxPasswordLength: 128,34    requireEmailVerification: true,35    autoSignIn: false,36    revokeSessionsOnPasswordReset: true,37    resetPasswordTokenExpiresIn: 60 * 60,38    password: {39      hash: (password) => argon2.hash(password, ARGON2_OPTS),40      verify: ({ hash, password }) => argon2.verify(hash, password).catch(() => false),41    },42    sendResetPassword: async ({ user, url }) => {43      await getEmailService().sendPasswordReset(user.email, url);44      await writeAudit({ userId: user.id, action: "password.reset_requested" });45    },46    onPasswordReset: async ({ user }) => {47      await writeAudit({ userId: user.id, action: "password.reset" });48      await getEmailService().sendPasswordChanged(user.email).catch(() => {});49    },50  },51  emailVerification: {52    sendOnSignUp: true,53    sendOnSignIn: true,54    autoSignInAfterVerification: true,55    expiresIn: 60 * 60 * 24,56    sendVerificationEmail: async ({ user, url }) => {57      await getEmailService().sendVerification(user.email, url);58    },59    afterEmailVerification: async (user) => {60      await writeAudit({ userId: user.id, action: "email.verified" });61      await getEmailService()62        .sendWelcome(user.email, user.name ?? "")63        .catch((e) => log.warn("welcome email failed", { error: (e as Error).message }));64    },65  },66  user: {67    additionalFields: {68      role: { type: "string", required: false, defaultValue: "user", input: false },69      onboardingCompletedAt: { type: "date", required: false, input: false },70    },71    changeEmail: {72      enabled: true,73      sendChangeEmailVerification: async ({ user, newEmail, url }: { user: { email: string; id: string }; newEmail: string; url: string }) => {74        await getEmailService().sendEmailChangeApproval(user.email, url, newEmail);75        await writeAudit({ userId: user.id, action: "email.change_requested" });76      },77    },78    deleteUser: {79      enabled: true,80      sendDeleteAccountVerification: async ({ user, url }) => {81        await getEmailService().sendAccountDeletion(user.email, url);82      },83      beforeDelete: async (user) => {84        await writeAudit({ userId: user.id, action: "account.deleted" });85      },86    },87  },88  session: {89    expiresIn: 60 * 60 * 24 * 30,90    updateAge: 60 * 60 * 24,91    cookieCache: { enabled: true, maxAge: 5 * 60 },92  },93  rateLimit: {94    enabled: true,95    window: 60,96    max: 100,97    customRules: {98      "/sign-in/email": { window: 60, max: 8 },99      "/sign-up/email": { window: 60, max: 4 },100      "/forget-password": { window: 60, max: 4 },101      "/request-password-reset": { window: 60, max: 4 },102      "/send-verification-email": { window: 60, max: 3 },103      "/change-password": { window: 60, max: 5 },104      "/change-email": { window: 60, max: 3 },105      "/delete-user": { window: 60, max: 3 },106    },107  },108  advanced: {109    cookiePrefix: "polyllm",110    useSecureCookies: IS_PROD,111  },112  databaseHooks: {113    user: {114      create: {115        after: async (user) => {116          await getDb().insert(userPreferences).values({ userId: user.id }).onConflictDoNothing();117          await writeAudit({ userId: user.id, action: "account.created" });118        },119      },120    },121    session: {122      create: {123        after: async (session) => {124          await writeAudit({125            userId: session.userId,126            action: "login",127            ipAddress: session.ipAddress ?? null,128            userAgent: session.userAgent ?? null,129          });130        },131      },132    },133  },134  plugins: [nextCookies()],135});136137export type Session = typeof auth.$Infer.Session;138export type AuthUser = Session["user"];139