diff --git a/backend/config.js b/backend/config.js index 02a7f25..c464036 100644 --- a/backend/config.js +++ b/backend/config.js @@ -1,17 +1,19 @@ require('dotenv').config(); -const TURNSTILE_SITE_KEY = - process.env.TURNSTILE_SITE_KEY || - process.env.TURNSTILE_SITEKEY || - process.env.VITE_TURNSTILE_SITE_KEY || - process.env.CLOUDFLARE_TURNSTILE_SITE_KEY || - ''; +const firstEnvValue = (...values) => values.find((value) => typeof value === 'string' && value.trim())?.trim() || ''; -const TURNSTILE_SECRET = - process.env.TURNSTILE_SECRET || - process.env.TURNSTILE_SECRET_KEY || - process.env.CLOUDFLARE_TURNSTILE_SECRET || - ''; +const TURNSTILE_SITE_KEY = firstEnvValue( + process.env.TURNSTILE_SITE_KEY, + process.env.TURNSTILE_SITEKEY, + process.env.VITE_TURNSTILE_SITE_KEY, + process.env.CLOUDFLARE_TURNSTILE_SITE_KEY +); + +const TURNSTILE_SECRET = firstEnvValue( + process.env.TURNSTILE_SECRET, + process.env.TURNSTILE_SECRET_KEY, + process.env.CLOUDFLARE_TURNSTILE_SECRET +); module.exports = { PORT: process.env.PORT || 3004, diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 95f5093..7a194a8 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -4,6 +4,8 @@ const { TURNSTILE_SECRET, TURNSTILE_SITE_KEY } = require('../config'); const router = express.Router(); const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; +const TURNSTILE_SITE_KEY_PATTERN = /^[0-9]x[0-9A-Za-z_-]{20,}$/; +const hasValidTurnstileSiteKey = TURNSTILE_SITE_KEY_PATTERN.test(TURNSTILE_SITE_KEY); const verifyCaptcha = async (captchaToken, remoteIp) => { if (!TURNSTILE_SECRET) return true; @@ -37,7 +39,8 @@ const verifyCaptcha = async (captchaToken, remoteIp) => { router.get('/login/config', (req, res) => { res.json({ captchaRequired: Boolean(TURNSTILE_SECRET), - turnstileSiteKey: TURNSTILE_SITE_KEY + turnstileSiteKey: hasValidTurnstileSiteKey ? TURNSTILE_SITE_KEY : '', + captchaConfigured: Boolean(TURNSTILE_SECRET && hasValidTurnstileSiteKey) }); }); diff --git a/src/dataService.ts b/src/dataService.ts index c189342..b1ef950 100644 --- a/src/dataService.ts +++ b/src/dataService.ts @@ -57,6 +57,7 @@ const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({ export type LoginConfig = { captchaRequired: boolean; + captchaConfigured: boolean; turnstileSiteKey: string; }; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 4ba48c6..35c5ff1 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -27,6 +27,7 @@ const securityUnavailableMessage = 'Não foi possível carregar a verificação const securityConfigurationMessage = 'Verificação de segurança indisponível. Entre em contato com o administrador.'; const securityRequiredMessage = 'Conclua a verificação de segurança para continuar.'; const securityFailedMessage = 'Não foi possível validar a verificação de segurança. Atualize a página e tente novamente.'; +const turnstileSiteKeyPattern = /^[0-9]x[0-9A-Za-z_-]{20,}$/; const Login = () => { const [email, setEmail] = useState(''); @@ -41,8 +42,9 @@ const Login = () => { const captchaContainerRef = useRef(null); const captchaWidgetIdRef = useRef(null); const navigate = useNavigate(); - const captchaEnabled = Boolean(captchaRequired && turnstileSiteKey); - const securityMisconfigured = captchaRequired && !turnstileSiteKey; + const hasValidSiteKey = turnstileSiteKeyPattern.test(turnstileSiteKey); + const captchaEnabled = Boolean(captchaRequired && hasValidSiteKey); + const securityMisconfigured = captchaRequired && !hasValidSiteKey; useEffect(() => { let isMounted = true; @@ -53,9 +55,9 @@ const Login = () => { if (!isMounted) return; setCaptchaRequired(config.captchaRequired); - setTurnstileSiteKey(config.turnstileSiteKey); + setTurnstileSiteKey(config.turnstileSiteKey.trim()); setCaptchaReady(!config.captchaRequired); - if (config.captchaRequired && !config.turnstileSiteKey) { + if (config.captchaRequired && !config.captchaConfigured) { setError(securityConfigurationMessage); } } catch { @@ -77,31 +79,37 @@ const Login = () => { }, []); useEffect(() => { - if (!turnstileSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return; + if (!hasValidSiteKey || !captchaContainerRef.current || captchaWidgetIdRef.current) return; const siteKey = turnstileSiteKey; const renderCaptcha = () => { if (!window.turnstile || !captchaContainerRef.current || captchaWidgetIdRef.current) return; - captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, { - sitekey: siteKey, - theme: 'dark', - callback: (token) => { - setCaptchaToken(token); - setCaptchaReady(true); - }, - 'expired-callback': () => { - setCaptchaToken(''); - setCaptchaReady(true); - }, - 'error-callback': () => { - setCaptchaToken(''); - setCaptchaReady(false); - setError(securityUnavailableMessage); - }, - }); - setCaptchaReady(true); + try { + captchaWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, { + sitekey: siteKey, + theme: 'dark', + callback: (token) => { + setCaptchaToken(token); + setCaptchaReady(true); + }, + 'expired-callback': () => { + setCaptchaToken(''); + setCaptchaReady(true); + }, + 'error-callback': () => { + setCaptchaToken(''); + setCaptchaReady(false); + setError(securityUnavailableMessage); + }, + }); + setCaptchaReady(true); + } catch { + setCaptchaToken(''); + setCaptchaReady(false); + setError(securityConfigurationMessage); + } }; if (window.turnstile) { @@ -128,7 +136,7 @@ const Login = () => { document.head.appendChild(script); return () => script.removeEventListener('load', renderCaptcha); - }, [turnstileSiteKey]); + }, [hasValidSiteKey, turnstileSiteKey]); const handleLogin = async (e: React.FormEvent) => { e.preventDefault();