From 75b505464cb0f414d3b9f1995eba4861aaacbb54 Mon Sep 17 00:00:00 2001 From: smanylov Date: Mon, 12 Jan 2026 14:07:06 +0700 Subject: [PATCH] add ActivityTracker --- src/app/[locale]/layout.tsx | 2 + src/app/components/ActivityTracker.tsx | 107 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/app/components/ActivityTracker.tsx diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index d684eaf..33d2e07 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -6,6 +6,7 @@ import { routing } from '@/i18n/routing'; import { ToastProvider } from '@/app/providers/ToastProvider'; import "../styles/globals.css"; import "../styles/global-styles.scss"; +import ActivityTracker from '../components/ActivityTracker'; export function generateStaticParams() { return routing.locales.map((locale: any) => ({ locale })); @@ -34,6 +35,7 @@ export default async function RootLayout({ {children} + diff --git a/src/app/components/ActivityTracker.tsx b/src/app/components/ActivityTracker.tsx new file mode 100644 index 0000000..b5ebad5 --- /dev/null +++ b/src/app/components/ActivityTracker.tsx @@ -0,0 +1,107 @@ +// components/ActivityTracker.tsx +'use client'; + +import { useEffect, useRef, useCallback } from 'react'; +import { tokenLifeExtension } from '@/app/actions/auth'; + +export default function ActivityTracker() { + const lastRefreshTime = useRef(Date.now()); + const activityTimeoutRef = useRef(null); + const isRefreshing = useRef(false); + + // Debounce функция + const debounce = useCallback((func: () => void, delay: number) => { + if (activityTimeoutRef.current) { + clearTimeout(activityTimeoutRef.current); + } + + activityTimeoutRef.current = setTimeout(func, delay); + }, []); + + // Ваша функция обновления токена (адаптированная для клиента) + const refreshTokenSilently = useCallback(async () => { + if (isRefreshing.current) return false; + + isRefreshing.current = true; + + try { + let response = tokenLifeExtension(); + + //@ts-ignore + return response.ok; + } catch (error) { + console.error('Silent token refresh failed:', error); + return false; + } finally { + isRefreshing.current = false; + } + }, []); + + // Проверка необходимости обновления + const checkAndRefresh = useCallback(async () => { + const now = Date.now(); + const timeSinceLastRefresh = now - lastRefreshTime.current; + + // Обновляем не чаще чем раз в 10 минут + // и только если прошло больше 5 минут с последнего обновления + if (timeSinceLastRefresh > 5 * 60 * 1000 && timeSinceLastRefresh > 30000) { + const refreshed = await refreshTokenSilently(); + if (refreshed) { + lastRefreshTime.current = now; + console.log('Token silently refreshed at', new Date().toISOString()); + } + } + }, [refreshTokenSilently]); + + // Обработчик активности + const handleActivity = useCallback(() => { + // Используем debounce чтобы не спамить запросами + // Ждем 30 секунд после последнего действия пользователя + debounce(checkAndRefresh, 30000); + }, [debounce, checkAndRefresh]); + + useEffect(() => { + const events = [ + 'mousedown', 'mousemove', 'wheel', 'keydown', + 'touchstart', 'touchend', 'scroll', 'click', + 'focus', 'blur', 'input', 'change' + ]; + + // Добавляем обработчики с passive: true для производительности + events.forEach(event => { + document.addEventListener(event, handleActivity, { passive: true }); + }); + + // Также обновляем при возвращении на вкладку + const handleVisibilityChange = () => { + if (!document.hidden) { + handleActivity(); + // Немедленно проверяем при возвращении на вкладку + setTimeout(checkAndRefresh, 1000); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Периодическая проверка (каждые 2 минуты) + const intervalId = setInterval(checkAndRefresh, 2 * 60 * 1000); + + // Инициализация - проверяем сразу если нужно + const initialCheck = setTimeout(checkAndRefresh, 10000); + + return () => { + // Очистка + events.forEach(event => { + document.removeEventListener(event, handleActivity); + }); + document.removeEventListener('visibilitychange', handleVisibilityChange); + clearInterval(intervalId); + clearTimeout(initialCheck); + if (activityTimeoutRef.current) { + clearTimeout(activityTimeoutRef.current); + } + }; + }, [handleActivity, checkAndRefresh]); + + return null; // Не рендерит ничего +} \ No newline at end of file