Skip to content

Commit fd203d1

Browse files
waleedlatif1claude
andcommitted
feat(auth): add execute-on-submit Turnstile, conditional harmony, and feature flag
- Switch Turnstile to execution: 'execute' mode so challenge runs on form submit (fresh token every time, no expiry issues) - Make emailHarmony conditional via SIGNUP_EMAIL_VALIDATION_ENABLED feature flag so self-hosted users can opt out - Add isSignupEmailValidationEnabled to feature-flags.ts following existing pattern - Add better-auth-harmony to Next.js transpilePackages (required for validator.js ESM compatibility) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6b10b0a commit fd203d1

File tree

6 files changed

+77
-9
lines changed

6 files changed

+77
-9
lines changed

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export default function LoginPage({
8989
const [showValidationError, setShowValidationError] = useState(false)
9090
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
9191
const turnstileRef = useRef<TurnstileInstance>(null)
92+
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
93+
const captchaRejectRef = useRef<((reason: unknown) => void) | null>(null)
9294
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
9395
const buttonClass = useBrandedButtonClass()
9496

@@ -182,6 +184,24 @@ export default function LoginPage({
182184
const safeCallbackUrl = callbackUrl
183185
let errorHandled = false
184186

187+
// Execute Turnstile challenge on submit and get a fresh token
188+
let token = captchaToken
189+
if (turnstileSiteKey && turnstileRef.current) {
190+
try {
191+
turnstileRef.current.reset()
192+
token = await new Promise<string>((resolve, reject) => {
193+
captchaResolveRef.current = resolve
194+
captchaRejectRef.current = reject
195+
turnstileRef.current?.execute()
196+
})
197+
} catch {
198+
setPasswordErrors(['Captcha verification failed. Please try again.'])
199+
setShowValidationError(true)
200+
setIsLoading(false)
201+
return
202+
}
203+
}
204+
185205
const result = await client.signIn.email(
186206
{
187207
email,
@@ -191,7 +211,7 @@ export default function LoginPage({
191211
{
192212
fetchOptions: {
193213
headers: {
194-
...(captchaToken ? { 'x-captcha-response': captchaToken } : {}),
214+
...(token ? { 'x-captcha-response': token } : {}),
195215
},
196216
},
197217
onError: (ctx) => {
@@ -475,10 +495,20 @@ export default function LoginPage({
475495
<Turnstile
476496
ref={turnstileRef}
477497
siteKey={turnstileSiteKey}
478-
onSuccess={setCaptchaToken}
479-
onError={() => setCaptchaToken(null)}
498+
onSuccess={(token) => {
499+
setCaptchaToken(token)
500+
captchaResolveRef.current?.(token)
501+
captchaResolveRef.current = null
502+
captchaRejectRef.current = null
503+
}}
504+
onError={() => {
505+
setCaptchaToken(null)
506+
captchaRejectRef.current?.(new Error('Captcha failed'))
507+
captchaResolveRef.current = null
508+
captchaRejectRef.current = null
509+
}}
480510
onExpire={() => setCaptchaToken(null)}
481-
options={{ size: 'invisible' }}
511+
options={{ size: 'invisible', execution: 'execute' }}
482512
/>
483513
)}
484514

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ function SignupFormContent({
9393
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
9494
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
9595
const turnstileRef = useRef<TurnstileInstance>(null)
96+
const captchaResolveRef = useRef<((token: string) => void) | null>(null)
97+
const captchaRejectRef = useRef<((reason: unknown) => void) | null>(null)
9698
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
9799
const buttonClass = useBrandedButtonClass()
98100

@@ -249,6 +251,24 @@ function SignupFormContent({
249251

250252
const sanitizedName = trimmedName
251253

254+
// Execute Turnstile challenge on submit and get a fresh token
255+
let token = captchaToken
256+
if (turnstileSiteKey && turnstileRef.current) {
257+
try {
258+
turnstileRef.current.reset()
259+
token = await new Promise<string>((resolve, reject) => {
260+
captchaResolveRef.current = resolve
261+
captchaRejectRef.current = reject
262+
turnstileRef.current?.execute()
263+
})
264+
} catch {
265+
setPasswordErrors(['Captcha verification failed. Please try again.'])
266+
setShowValidationError(true)
267+
setIsLoading(false)
268+
return
269+
}
270+
}
271+
252272
const response = await client.signUp.email(
253273
{
254274
email: emailValue,
@@ -258,7 +278,7 @@ function SignupFormContent({
258278
{
259279
fetchOptions: {
260280
headers: {
261-
...(captchaToken ? { 'x-captcha-response': captchaToken } : {}),
281+
...(token ? { 'x-captcha-response': token } : {}),
262282
},
263283
},
264284
onError: (ctx) => {
@@ -468,10 +488,20 @@ function SignupFormContent({
468488
<Turnstile
469489
ref={turnstileRef}
470490
siteKey={turnstileSiteKey}
471-
onSuccess={setCaptchaToken}
472-
onError={() => setCaptchaToken(null)}
491+
onSuccess={(token) => {
492+
setCaptchaToken(token)
493+
captchaResolveRef.current?.(token)
494+
captchaResolveRef.current = null
495+
captchaRejectRef.current = null
496+
}}
497+
onError={() => {
498+
setCaptchaToken(null)
499+
captchaRejectRef.current?.(new Error('Captcha failed'))
500+
captchaResolveRef.current = null
501+
captchaRejectRef.current = null
502+
}}
473503
onExpire={() => setCaptchaToken(null)}
474-
options={{ size: 'invisible' }}
504+
options={{ size: 'invisible', execution: 'execute' }}
475505
/>
476506
)}
477507

apps/sim/lib/auth/auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
isHosted,
6666
isOrganizationsEnabled,
6767
isRegistrationDisabled,
68+
isSignupEmailValidationEnabled,
6869
} from '@/lib/core/config/feature-flags'
6970
import { PlatformEvents } from '@/lib/core/telemetry'
7071
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -664,7 +665,7 @@ export const auth = betterAuth({
664665
},
665666
plugins: [
666667
nextCookies(),
667-
emailHarmony(),
668+
...(isSignupEmailValidationEnabled ? [emailHarmony()] : []),
668669
...(env.TURNSTILE_SECRET_KEY
669670
? [
670671
captcha({

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const env = createEnv({
2626
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login
2727
BLOCKED_SIGNUP_DOMAINS: z.string().optional(), // Comma-separated list of email domains blocked from signing up (e.g., "gmail.com,yahoo.com")
2828
TURNSTILE_SECRET_KEY: z.string().min(1).optional(), // Cloudflare Turnstile secret key for captcha verification
29+
SIGNUP_EMAIL_VALIDATION_ENABLED: z.boolean().optional(), // Enable disposable email blocking via better-auth-harmony (55K+ domains)
2930
ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data
3031
API_ENCRYPTION_KEY: z.string().min(32).optional(), // Dedicated key for encrypting API keys (optional for OSS)
3132
INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export const isRegistrationDisabled = isTruthy(env.DISABLE_REGISTRATION)
7070
*/
7171
export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED)
7272

73+
/**
74+
* Is signup email validation enabled (disposable email blocking via better-auth-harmony)
75+
*/
76+
export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED)
77+
7378
/**
7479
* Is Trigger.dev enabled for async job processing
7580
*/

apps/sim/next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const nextConfig: NextConfig = {
120120
'@t3-oss/env-nextjs',
121121
'@t3-oss/env-core',
122122
'@sim/db',
123+
'better-auth-harmony',
123124
],
124125
async headers() {
125126
return [

0 commit comments

Comments
 (0)