Skip to content

Commit 2d88713

Browse files
committed
fix: make Turnstile fully optional for self-hosted deployments
- Lazy-load @marsidev/react-turnstile via React.lazy so the package is never fetched or bundled when VITE_TURNSTILE_SITE_KEY is not set - Move TURNSTILE_ENABLED check to module scope for tree-shaking - Update AuthContext signIn/signUp signatures to accept optional captchaToken (string | null | undefined) instead of requiring string - Pass captchaToken || undefined to Supabase auth options so null is never sent to the API - Main bundle reduced ~6 kB for self-hosted builds without Turnstile
1 parent e9f6ab9 commit 2d88713

File tree

2 files changed

+43
-33
lines changed

2 files changed

+43
-33
lines changed

src/contexts/AuthContext.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ interface AuthContextType {
3939
profile: Profile | null;
4040
tenant: TenantInfo | null;
4141
loading: boolean;
42-
signIn: (email: string, password: string, captchaToken: string) => Promise<{ error: Error | null }>;
43-
signUp: (email: string, password: string, userData: Partial<Profile> & { company_name?: string }, captchaToken: string) => Promise<{ error: Error | null; data?: any }>;
42+
signIn: (email: string, password: string, captchaToken?: string | null) => Promise<{ error: Error | null }>;
43+
signUp: (email: string, password: string, userData: Partial<Profile> & { company_name?: string }, captchaToken?: string | null) => Promise<{ error: Error | null; data?: any }>;
4444
signOut: () => Promise<void>;
4545
switchTenant: (tenantId: string) => Promise<void>;
4646
refreshTenant: () => Promise<void>;
@@ -166,13 +166,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
166166
await fetchTenant();
167167
};
168168

169-
const signIn = async (email: string, password: string, captchaToken: string) => {
169+
const signIn = async (email: string, password: string, captchaToken?: string | null) => {
170170
try {
171171
const { error } = await supabase.auth.signInWithPassword({
172172
email,
173173
password,
174174
options: {
175-
captchaToken,
175+
captchaToken: captchaToken || undefined,
176176
},
177177
});
178178
return { error };
@@ -185,7 +185,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
185185
email: string,
186186
password: string,
187187
userData: Partial<Profile> & { company_name?: string },
188-
captchaToken: string
188+
captchaToken?: string | null
189189
) => {
190190
try {
191191
// Generate username from email (part before @)
@@ -196,7 +196,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
196196
password,
197197
options: {
198198
emailRedirectTo: `${window.location.origin}/`,
199-
captchaToken,
199+
captchaToken: captchaToken || undefined,
200200
data: {
201201
username,
202202
full_name: userData.full_name,

src/pages/auth/Auth.tsx

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useRef } from "react";
1+
import { useState, useRef, lazy, Suspense } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { useTranslation } from "react-i18next";
44
import { useAuth } from "@/contexts/AuthContext";
@@ -12,7 +12,15 @@ import { LanguageSwitcher } from "@/components/LanguageSwitcher";
1212
import AnimatedBackground from "@/components/AnimatedBackground";
1313
import { Link } from "react-router-dom";
1414
import { ROUTES } from "@/routes";
15-
import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile";
15+
16+
// Lazy-load Turnstile — only fetched when VITE_TURNSTILE_SITE_KEY is set.
17+
// Self-hosted deployments without Turnstile pay zero bundle cost.
18+
const TURNSTILE_ENABLED = Boolean(import.meta.env.VITE_TURNSTILE_SITE_KEY);
19+
const LazyTurnstile = TURNSTILE_ENABLED
20+
? lazy(() =>
21+
import("@marsidev/react-turnstile").then((m) => ({ default: m.Turnstile }))
22+
)
23+
: null;
1624

1725
export default function Auth() {
1826
const { t } = useTranslation();
@@ -27,10 +35,10 @@ export default function Auth() {
2735
const [error, setError] = useState<string | null>(null);
2836
const [success, setSuccess] = useState<string | null>(null);
2937
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
30-
const turnstileRef = useRef<TurnstileInstance>(null);
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
const turnstileRef = useRef<any>(null);
3140
const { signIn, signUp, profile } = useAuth();
3241
const navigate = useNavigate();
33-
const turnstileEnabled = Boolean(import.meta.env.VITE_TURNSTILE_SITE_KEY);
3442

3543
// Redirect if already logged in
3644
if (profile) {
@@ -50,7 +58,7 @@ export default function Auth() {
5058

5159
try {
5260
// Validate captcha token (only when Turnstile is enabled)
53-
if (turnstileEnabled && !captchaToken) {
61+
if (TURNSTILE_ENABLED && !captchaToken) {
5462
setError(t("auth.captchaRequired"));
5563
setLoading(false);
5664
turnstileRef.current?.reset();
@@ -294,34 +302,36 @@ export default function Auth() {
294302
</Alert>
295303
)}
296304

297-
{/* Cloudflare Turnstile Captcha - only rendered when site key is configured */}
298-
{turnstileEnabled && (
299-
<div className="flex justify-center">
300-
<Turnstile
301-
ref={turnstileRef}
302-
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY!}
303-
onSuccess={(token) => setCaptchaToken(token)}
304-
onError={() => {
305-
setError(t("auth.captchaError"));
306-
setCaptchaToken(null);
307-
}}
308-
onExpire={() => {
309-
setCaptchaToken(null);
310-
turnstileRef.current?.reset();
311-
}}
312-
options={{
313-
theme: "dark",
314-
size: "normal",
315-
}}
316-
/>
317-
</div>
305+
{/* Cloudflare Turnstile Captcha — only loaded when VITE_TURNSTILE_SITE_KEY is set */}
306+
{TURNSTILE_ENABLED && LazyTurnstile && (
307+
<Suspense fallback={<div className="flex justify-center py-2"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>}>
308+
<div className="flex justify-center">
309+
<LazyTurnstile
310+
ref={turnstileRef}
311+
siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY!}
312+
onSuccess={(token: string) => setCaptchaToken(token)}
313+
onError={() => {
314+
setError(t("auth.captchaError"));
315+
setCaptchaToken(null);
316+
}}
317+
onExpire={() => {
318+
setCaptchaToken(null);
319+
turnstileRef.current?.reset();
320+
}}
321+
options={{
322+
theme: "dark",
323+
size: "normal",
324+
}}
325+
/>
326+
</div>
327+
</Suspense>
318328
)}
319329

320330
<div className="pt-2">
321331
<Button
322332
type="submit"
323333
className="w-full cta-button"
324-
disabled={loading || (!isLogin && !termsAgreed) || (turnstileEnabled && !captchaToken)}
334+
disabled={loading || (!isLogin && !termsAgreed) || (TURNSTILE_ENABLED && !captchaToken)}
325335
>
326336
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
327337
{isLogin ? t("auth.signIn") : t("auth.signUp")}

0 commit comments

Comments
 (0)