fix: stop password keystrokes from triggering API calls on public dashboard

Used refs for password/captcha values so loadDashboard doesn't
recreate on every keystroke. Password is only sent to API on
explicit form submit. Also fixes stale captcha state in closures.
This commit is contained in:
Usman Baig
2026-03-22 13:52:10 +01:00
parent ef21004519
commit 82a201a043

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image' import Image from 'next/image'
import { useParams, useSearchParams, useRouter } from 'next/navigation' import { useParams, useSearchParams, useRouter } from 'next/navigation'
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, type DashboardData, type Stats, type DailyStat } from '@/lib/api/stats' import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, type DashboardData, type Stats, type DailyStat } from '@/lib/api/stats'
@@ -39,6 +39,7 @@ export default function PublicDashboardPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [data, setData] = useState<DashboardData | null>(null) const [data, setData] = useState<DashboardData | null>(null)
const [password, setPassword] = useState(passwordParam || '') const [password, setPassword] = useState(passwordParam || '')
const [submittedPassword, setSubmittedPassword] = useState(passwordParam || '')
const [isPasswordProtected, setIsPasswordProtected] = useState(false) const [isPasswordProtected, setIsPasswordProtected] = useState(false)
// Captcha State // Captcha State
@@ -92,11 +93,11 @@ export default function PublicDashboardPage() {
const loadRealtime = useCallback(async () => { const loadRealtime = useCallback(async () => {
try { try {
const auth = { const auth = {
password, password: passwordRef.current,
captcha: { captcha: {
captcha_id: captchaId, captcha_id: captchaIdRef.current,
captcha_solution: captchaSolution, captcha_solution: captchaSolutionRef.current,
captcha_token: captchaToken captcha_token: captchaTokenRef.current
} }
} }
const realtimeData = await getPublicRealtime(siteId, auth) const realtimeData = await getPublicRealtime(siteId, auth)
@@ -109,19 +110,30 @@ export default function PublicDashboardPage() {
} catch (error) { } catch (error) {
// Silently fail for realtime updates // Silently fail for realtime updates
} }
}, [siteId, password, captchaId, captchaSolution, captchaToken, data]) }, [siteId, data])
// Use refs for auth values so loadDashboard doesn't recreate on every keystroke/captcha change
const passwordRef = useRef(submittedPassword)
const captchaIdRef = useRef(captchaId)
const captchaSolutionRef = useRef(captchaSolution)
const captchaTokenRef = useRef(captchaToken)
passwordRef.current = submittedPassword
captchaIdRef.current = captchaId
captchaSolutionRef.current = captchaSolution
captchaTokenRef.current = captchaToken
const loadDashboard = useCallback(async (silent = false) => { const loadDashboard = useCallback(async (silent = false) => {
try { try {
if (!silent) setLoading(true) if (!silent) setLoading(true)
const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval
const pw = passwordRef.current
const auth = { const auth = {
password, password: pw,
captcha: { captcha: {
captcha_id: captchaId, captcha_id: captchaIdRef.current,
captcha_solution: captchaSolution, captcha_solution: captchaSolutionRef.current,
captcha_token: captchaToken captcha_token: captchaTokenRef.current
} }
} }
@@ -132,7 +144,7 @@ export default function PublicDashboardPage() {
dateRange.end, dateRange.end,
10, 10,
interval, interval,
password, pw,
auth.captcha auth.captcha
), ),
(async () => { (async () => {
@@ -144,7 +156,7 @@ export default function PublicDashboardPage() {
return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth) return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth)
})() })()
]) ])
setData(dashboardData) setData(dashboardData)
setPrevStats(prevStatsData) setPrevStats(prevStatsData)
setPrevDailyStats(prevDailyStatsData) setPrevDailyStats(prevDailyStatsData)
@@ -159,7 +171,7 @@ export default function PublicDashboardPage() {
const apiErr = error instanceof ApiError ? error : null const apiErr = error instanceof ApiError ? error : null
if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) { if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) {
setIsPasswordProtected(true) setIsPasswordProtected(true)
if (password) { if (passwordRef.current) {
toast.error('Invalid password or captcha') toast.error('Invalid password or captcha')
// Reset captcha on failure // Reset captcha on failure
setCaptchaId('') setCaptchaId('')
@@ -174,7 +186,7 @@ export default function PublicDashboardPage() {
} finally { } finally {
if (!silent) setLoading(false) if (!silent) setLoading(false)
} }
}, [siteId, dateRange, todayInterval, multiDayInterval, password, captchaId, captchaSolution, captchaToken]) }, [siteId, dateRange, todayInterval, multiDayInterval])
// * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds // * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds
useEffect(() => { useEffect(() => {
@@ -185,7 +197,7 @@ export default function PublicDashboardPage() {
}, 30000) }, 30000)
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime]) }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, loadDashboard, loadRealtime])
useEffect(() => { useEffect(() => {
loadDashboard() loadDashboard()
@@ -193,6 +205,9 @@ export default function PublicDashboardPage() {
const handlePasswordSubmit = (e: React.FormEvent) => { const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
// Update ref immediately so loadDashboard reads the latest password
passwordRef.current = password
setSubmittedPassword(password)
loadDashboard() loadDashboard()
} }