- Add captcha plugin to Better Auth config (cloudflare-turnstile provider) - Create reusable TurnstileWidget component with script injection - Integrate Turnstile into login, signup, and forgot-password forms - Pass x-captcha-response header on protected endpoints - Add TURNSTILE_SECRET_KEY and NEXT_PUBLIC_TURNSTILE_SITE_KEY to env
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
import { betterAuth } from 'better-auth'
|
|
import { admin, captcha, emailOTP, lastLoginMethod } from 'better-auth/plugins'
|
|
import { passkey } from '@better-auth/passkey'
|
|
import { expo } from '@better-auth/expo'
|
|
import { drizzleAdapter } from '@better-auth/drizzle-adapter'
|
|
import { db } from '@/lib/db/index'
|
|
import { ac, admin as adminRole, moderator, contributor, user } from '@/lib/auth/permissions'
|
|
import { sendOTP, OTP_EXPIRY_SECONDS } from '@/lib/auth/email'
|
|
|
|
// ── Origin setup ───────────────────────────────────────
|
|
// Web + mobile passkey origins
|
|
const webOrigin = process.env.BETTER_AUTH_URL ?? 'https://localhost:3000'
|
|
const appScheme = 'deckyvault://'
|
|
|
|
// Build trusted origins: web URL + app scheme + optional expo dev
|
|
const trustedOrigins = [
|
|
webOrigin,
|
|
appScheme,
|
|
]
|
|
|
|
// Add Expo dev URLs in development
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
trustedOrigins.push('exp://*')
|
|
trustedOrigins.push('exp://192.168.*.*:*')
|
|
}
|
|
|
|
// Passkey origins: web URL + optional Android APK key hash
|
|
const passkeyOrigins = [
|
|
webOrigin,
|
|
...(process.env.ANDROID_APK_KEY_HASH
|
|
? [`android:apk-key-hash:${process.env.ANDROID_APK_KEY_HASH}`]
|
|
: []),
|
|
]
|
|
|
|
export const auth = betterAuth({
|
|
experimental: { joins: true },
|
|
database: drizzleAdapter(db, {
|
|
provider: 'pg'
|
|
}),
|
|
plugins: [
|
|
captcha({
|
|
provider: 'cloudflare-turnstile',
|
|
secretKey: process.env.TURNSTILE_SECRET_KEY!,
|
|
}),
|
|
emailOTP({
|
|
async sendVerificationOTP({ email, otp, type }) {
|
|
await sendOTP({ email, otp, type })
|
|
},
|
|
otpLength: 6,
|
|
expiresIn: OTP_EXPIRY_SECONDS,
|
|
allowedAttempts: 5,
|
|
}),
|
|
passkey({
|
|
rpID: process.env.RP_ID ?? 'localhost',
|
|
rpName: 'DeckyVault',
|
|
origin: passkeyOrigins,
|
|
advanced: {
|
|
webAuthnChallengeCookie: 'better-auth-passkey',
|
|
},
|
|
}),
|
|
lastLoginMethod({
|
|
storeInDatabase: true,
|
|
}),
|
|
admin({
|
|
ac,
|
|
roles: {
|
|
admin: adminRole,
|
|
moderator,
|
|
contributor,
|
|
user,
|
|
},
|
|
defaultRole: 'user',
|
|
adminRoles: ['admin'],
|
|
}),
|
|
expo(),
|
|
],
|
|
socialProviders: {
|
|
google: {
|
|
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
|
},
|
|
discord: {
|
|
clientId: process.env.DISCORD_CLIENT_ID || "",
|
|
clientSecret: process.env.DISCORD_CLIENT_SECRET || "",
|
|
},
|
|
},
|
|
trustedOrigins,
|
|
rateLimit: {
|
|
enabled: false,
|
|
},
|
|
session: {
|
|
expiresIn: 60 * 60 * 24 * 7,
|
|
updateAge: 60 * 60 * 24,
|
|
cookieCache: {
|
|
enabled: true,
|
|
maxAge: 15 * 60,
|
|
},
|
|
},
|
|
account: {
|
|
accountLinking: {
|
|
enabled: true,
|
|
trustedProviders: ['google', 'discord'],
|
|
allowDifferentEmails: true
|
|
}
|
|
},
|
|
emailAndPassword: {
|
|
enabled: true,
|
|
},
|
|
})
|