2026-06-23 20:36:07 -04:00
|
|
|
import { betterAuth } from "better-auth"
|
|
|
|
|
import { drizzleAdapter } from "better-auth/adapters/drizzle"
|
|
|
|
|
import { nextCookies } from "better-auth/next-js"
|
|
|
|
|
import { admin } from "better-auth/plugins"
|
|
|
|
|
import { db } from "@/lib/db"
|
|
|
|
|
import { user, session, account, verification, profiles } from "@/lib/db/schema"
|
2026-07-02 13:42:34 -04:00
|
|
|
import { sendEmail, resetPasswordHtml, verifyEmailHtml } from "@/lib/email/send"
|
2026-06-23 20:36:07 -04:00
|
|
|
|
|
|
|
|
// Bootstrap superadmins from env — no API path lets a user self-promote.
|
|
|
|
|
const ADMIN_USER_IDS = (process.env.ADMIN_USER_IDS ?? "")
|
|
|
|
|
.split(",")
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
|
|
export const auth = betterAuth({
|
|
|
|
|
baseURL: process.env.BETTER_AUTH_URL,
|
|
|
|
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
|
|
|
database: drizzleAdapter(db, {
|
|
|
|
|
provider: "pg",
|
|
|
|
|
schema: { user, session, account, verification },
|
|
|
|
|
}),
|
|
|
|
|
emailAndPassword: {
|
|
|
|
|
enabled: true,
|
2026-07-01 13:56:34 -04:00
|
|
|
// Env-gated so production can require a verified email without breaking
|
2026-07-02 13:42:34 -04:00
|
|
|
// local dev (where SMTP is typically unconfigured). Set
|
2026-07-01 13:56:34 -04:00
|
|
|
// REQUIRE_EMAIL_VERIFICATION=true in production to enforce.
|
|
|
|
|
requireEmailVerification: process.env.REQUIRE_EMAIL_VERIFICATION === "true",
|
2026-06-23 20:36:07 -04:00
|
|
|
minPasswordLength: 8,
|
|
|
|
|
sendResetPassword: async ({ user: u, url }) => {
|
|
|
|
|
await sendEmail({
|
|
|
|
|
to: u.email,
|
|
|
|
|
subject: "Reset your Property Management Network password",
|
|
|
|
|
html: resetPasswordHtml(url),
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-01 13:56:34 -04:00
|
|
|
// Send a verification email on sign-up. Enforcement of verified-email login
|
|
|
|
|
// is gated by REQUIRE_EMAIL_VERIFICATION (see emailAndPassword above).
|
|
|
|
|
emailVerification: {
|
|
|
|
|
sendOnSignUp: true,
|
2026-07-02 13:42:34 -04:00
|
|
|
// After the user clicks the verification link, sign them in and send them
|
|
|
|
|
// to the callbackURL (set to /dashboard on sign-up).
|
|
|
|
|
autoSignInAfterVerification: true,
|
2026-07-01 13:56:34 -04:00
|
|
|
sendVerificationEmail: async ({ user: u, url }) => {
|
|
|
|
|
await sendEmail({
|
|
|
|
|
to: u.email,
|
|
|
|
|
subject: "Verify your email — Property Management Network",
|
|
|
|
|
html: verifyEmailHtml(url),
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-06-23 20:36:07 -04:00
|
|
|
socialProviders: {
|
|
|
|
|
google: {
|
|
|
|
|
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
|
|
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Throttle auth endpoints (per IP) to slow brute-force / credential stuffing.
|
|
|
|
|
rateLimit: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
window: 60, // seconds
|
|
|
|
|
max: 20, // requests per window per IP for auth endpoints
|
2026-07-01 13:56:34 -04:00
|
|
|
customRules: {
|
|
|
|
|
// Tighter limit on the password sign-in endpoint to slow credential
|
|
|
|
|
// stuffing / brute-force attempts.
|
|
|
|
|
"/sign-in/email": { window: 60, max: 10 },
|
|
|
|
|
},
|
2026-06-23 20:36:07 -04:00
|
|
|
},
|
|
|
|
|
// Auto-create the app `profiles` row whenever Better Auth creates a user
|
|
|
|
|
// (replaces the old `handle_new_user` Postgres trigger).
|
|
|
|
|
databaseHooks: {
|
|
|
|
|
user: {
|
|
|
|
|
create: {
|
|
|
|
|
after: async (u) => {
|
|
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.insert(profiles)
|
|
|
|
|
.values({ id: u.id, email: u.email, full_name: u.name ?? null })
|
|
|
|
|
.onConflictDoNothing()
|
|
|
|
|
} catch {
|
|
|
|
|
// Never block sign-up on profile creation.
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// `admin` enables role/ban/impersonation; `nextCookies` MUST stay last.
|
|
|
|
|
plugins: [
|
|
|
|
|
admin({ adminUserIds: ADMIN_USER_IDS }),
|
|
|
|
|
nextCookies(),
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
|