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'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import { useParams, useSearchParams, useRouter } from 'next/navigation'
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 [data, setData] = useState<DashboardData | null>(null)
const [password, setPassword] = useState(passwordParam || '')
const [submittedPassword, setSubmittedPassword] = useState(passwordParam || '')
const [isPasswordProtected, setIsPasswordProtected] = useState(false)
// Captcha State
@@ -92,11 +93,11 @@ export default function PublicDashboardPage() {
const loadRealtime = useCallback(async () => {
try {
const auth = {
password,
password: passwordRef.current,
captcha: {
captcha_id: captchaId,
captcha_solution: captchaSolution,
captcha_token: captchaToken
captcha_id: captchaIdRef.current,
captcha_solution: captchaSolutionRef.current,
captcha_token: captchaTokenRef.current
}
}
const realtimeData = await getPublicRealtime(siteId, auth)
@@ -109,19 +110,30 @@ export default function PublicDashboardPage() {
} catch (error) {
// 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) => {
try {
if (!silent) setLoading(true)
const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval
const pw = passwordRef.current
const auth = {
password,
password: pw,
captcha: {
captcha_id: captchaId,
captcha_solution: captchaSolution,
captcha_token: captchaToken
captcha_id: captchaIdRef.current,
captcha_solution: captchaSolutionRef.current,
captcha_token: captchaTokenRef.current
}
}
@@ -132,7 +144,7 @@ export default function PublicDashboardPage() {
dateRange.end,
10,
interval,
password,
pw,
auth.captcha
),
(async () => {
@@ -144,7 +156,7 @@ export default function PublicDashboardPage() {
return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth)
})()
])
setData(dashboardData)
setPrevStats(prevStatsData)
setPrevDailyStats(prevDailyStatsData)
@@ -159,7 +171,7 @@ export default function PublicDashboardPage() {
const apiErr = error instanceof ApiError ? error : null
if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) {
setIsPasswordProtected(true)
if (password) {
if (passwordRef.current) {
toast.error('Invalid password or captcha')
// Reset captcha on failure
setCaptchaId('')
@@ -174,7 +186,7 @@ export default function PublicDashboardPage() {
} finally {
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
useEffect(() => {
@@ -185,7 +197,7 @@ export default function PublicDashboardPage() {
}, 30000)
return () => clearInterval(interval)
}
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime])
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, loadDashboard, loadRealtime])
useEffect(() => {
loadDashboard()
@@ -193,6 +205,9 @@ export default function PublicDashboardPage() {
const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Update ref immediately so loadDashboard reads the latest password
passwordRef.current = password
setSubmittedPassword(password)
loadDashboard()
}