Files
deckyvault/lib/auth.ts
T
adrianbonpin ec5ee7fb18 feat: add Cloudflare Turnstile captcha to auth flow
- 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
2026-06-26 21:50:45 +08:00

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,
},
})