diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx index e43966c..780c17f 100644 --- a/app/share/[id]/page.tsx +++ b/app/share/[id]/page.tsx @@ -3,7 +3,7 @@ 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' +import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, authenticatePublicDashboard, type DashboardData, type Stats, type DailyStat } from '@/lib/api/stats' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { ApiError } from '@/lib/api/client' @@ -39,9 +39,10 @@ export default function PublicDashboardPage() { const [loading, setLoading] = useState(true) const [data, setData] = useState(null) const [password, setPassword] = useState(passwordParam || '') - const [submittedPassword, setSubmittedPassword] = useState(passwordParam || '') const [isPasswordProtected, setIsPasswordProtected] = useState(false) - + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [authLoading, setAuthLoading] = useState(false) + // Captcha State const [captchaId, setCaptchaId] = useState('') const [captchaSolution, setCaptchaSolution] = useState('') @@ -92,68 +93,30 @@ export default function PublicDashboardPage() { const loadRealtime = useCallback(async () => { try { - const auth = { - password: passwordRef.current, - captcha: { - captcha_id: captchaIdRef.current, - captcha_solution: captchaSolutionRef.current, - captcha_token: captchaTokenRef.current - } - } - const realtimeData = await getPublicRealtime(siteId, auth) + const realtimeData = await getPublicRealtime(siteId) if (data) { - setData({ - ...data, - realtime_visitors: realtimeData.visitors - }) + setData({ ...data, realtime_visitors: realtimeData.visitors }) } - } catch (error) { + } catch { // Silently fail for realtime updates } }, [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: pw, - captcha: { - captcha_id: captchaIdRef.current, - captcha_solution: captchaSolutionRef.current, - captcha_token: captchaTokenRef.current - } - } const [dashboardData, prevStatsData, prevDailyStatsData] = await Promise.all([ - getPublicDashboard( - siteId, - dateRange.start, - dateRange.end, - 10, - interval, - pw, - auth.captcha - ), + getPublicDashboard(siteId, dateRange.start, dateRange.end, 10, interval), (async () => { const prevRange = getPreviousDateRange(dateRange.start, dateRange.end) - return getPublicStats(siteId, prevRange.start, prevRange.end, auth) + return getPublicStats(siteId, prevRange.start, prevRange.end) })(), (async () => { const prevRange = getPreviousDateRange(dateRange.start, dateRange.end) - return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth) + return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval) })() ]) @@ -161,23 +124,11 @@ export default function PublicDashboardPage() { setPrevStats(prevStatsData) setPrevDailyStats(prevDailyStatsData) setLastUpdatedAt(Date.now()) - setIsPasswordProtected(false) - // Reset captcha - setCaptchaId('') - setCaptchaSolution('') - setCaptchaToken('') } catch (error: unknown) { const apiErr = error instanceof ApiError ? error : null if (apiErr?.status === 401 && (apiErr.data as Record)?.is_protected) { setIsPasswordProtected(true) - if (passwordRef.current) { - toast.error('Invalid password or captcha') - // Reset captcha on failure - setCaptchaId('') - setCaptchaSolution('') - setCaptchaToken('') - } } else if (apiErr?.status === 404) { toast.error('Site not found') } else if (!silent) { @@ -203,12 +154,30 @@ export default function PublicDashboardPage() { loadDashboard() }, [siteId, dateRange, todayInterval, multiDayInterval, loadDashboard]) - const handlePasswordSubmit = (e: React.FormEvent) => { + const handlePasswordSubmit = async (e: React.FormEvent) => { e.preventDefault() - // Update ref immediately so loadDashboard reads the latest password - passwordRef.current = password - setSubmittedPassword(password) - loadDashboard() + setAuthLoading(true) + try { + await authenticatePublicDashboard(siteId, password, captchaToken, captchaId, captchaSolution) + // Cookie is now set — load dashboard (cookie sent automatically) + setIsAuthenticated(true) + await loadDashboard() + } catch (error: unknown) { + const apiErr = error instanceof ApiError ? error : null + if (apiErr?.status === 401) { + const errData = apiErr.data as Record | undefined + const errMsg = errData?.error as string | undefined + toast.error(errMsg || 'Invalid password or captcha') + } else { + toast.error('Authentication failed') + } + // Reset captcha on failure + setCaptchaId('') + setCaptchaSolution('') + setCaptchaToken('') + } finally { + setAuthLoading(false) + } } const showSkeleton = useMinimumLoading(loading && !data && !isPasswordProtected) diff --git a/lib/api/stats.ts b/lib/api/stats.ts index a26fa04..9897d6c 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -117,6 +117,21 @@ export interface FrustrationByPage { unique_elements: number } +// ─── Public Auth ───────────────────────────────────────────────────── + +export function authenticatePublicDashboard(siteId: string, password: string, captchaToken?: string, captchaId?: string, captchaSolution?: string): Promise<{ status: string }> { + return apiRequest<{ status: string }>(`/public/sites/${siteId}/auth`, { + method: 'POST', + body: JSON.stringify({ + password, + captcha_token: captchaToken || '', + captcha_id: captchaId || '', + captcha_solution: captchaSolution || '', + }), + credentials: 'include', + }) +} + // ─── Helpers ──────────────────────────────────────────────────────── function appendAuthParams(params: URLSearchParams, auth?: AuthParams) {