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:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user