"use client"; import { useEffect, useState, useRef, useCallback } from "react"; import { createBrowserClient } from "@supabase/ssr"; import { useRouter } from "next/navigation"; import { signOut } from "@/lib/actions/auth"; import { resolveSupabaseUrl } from "@/lib/utils"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Clock } from "lucide-react"; // Inaktivitäts-Schwellenwert (10 Minuten = 600.000 ms) const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // Countdown-Dauer vor automatischem Logout (10 Sekunden) const COUNTDOWN_SECONDS = 10; export function InactivityTracker() { const router = useRouter(); const [isLoggedIn, setIsLoggedIn] = useState(false); const [showModal, setShowModal] = useState(false); const [countdown, setCountdown] = useState(COUNTDOWN_SECONDS); const lastActivityRef = useRef(Date.now()); const countdownIntervalRef = useRef(null); // Supabase URL & Anon Key auslesen (für Browser-Client) const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; const supabase = createBrowserClient( resolveSupabaseUrl(supabaseUrl)!, supabaseAnonKey!, { cookieOptions: { name: "webshop-auth-token", }, } ); // Cookie-Hilfsfunktionen für Aktivitätssynchronisation mit dem Server const updateLastActivityCookie = useCallback(() => { const now = Date.now(); lastActivityRef.current = now; document.cookie = `webshop-last-activity=${now}; path=/; SameSite=Lax`; }, []); const getLastActivityFromCookie = useCallback((): number => { const match = document.cookie.match(/(?:^|; )webshop-last-activity=([^;]*)/); if (match && match[1]) { const val = parseInt(match[1], 10); if (!isNaN(val)) return val; } return lastActivityRef.current; }, []); const handleLogout = useCallback(async (reason = "inactivity") => { if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); } setShowModal(false); document.cookie = "webshop-last-activity=; path=/; max-age=0"; try { await signOut(); } catch (e) { console.error("Fehler beim Server-Signout:", e); } await supabase.auth.signOut(); router.push(`/auth/login?message=${reason}`); router.refresh(); }, [supabase, router]); // Session-Prüfung & Inaktivitäts-Überwachung useEffect(() => { // 1. Session prüfen const checkSession = async () => { const { data: { session } } = await supabase.auth.getSession(); setIsLoggedIn(!!session); }; checkSession(); // 2. Regelmäßige Intervall-Prüfung (alle 5s) const interval = setInterval(async () => { const { data: { session } } = await supabase.auth.getSession(); if (!session) { setIsLoggedIn(false); return; } setIsLoggedIn(true); // Prüfen, ob Session auf Server noch gültig ist const { data: { user }, error } = await supabase.auth.getUser(); if (error) { const isAuthError = error.status === 400 || error.status === 401 || error.status === 403; if (isAuthError) { handleLogout("concurrent"); return; } } else if (!user) { handleLogout("concurrent"); return; } // Inaktivitätszeitraum (Client + Cookie-Synchronisation) prüfen const lastActive = getLastActivityFromCookie(); const now = Date.now(); if (!showModal && now - lastActive >= INACTIVITY_TIMEOUT_MS) { setShowModal(true); setCountdown(COUNTDOWN_SECONDS); } }, 5000); return () => { clearInterval(interval); }; }, [supabase, handleLogout, showModal, getLastActivityFromCookie]); // Aktivitätsevents registrieren useEffect(() => { if (!isLoggedIn) return; let lastEventTime = 0; const handleUserActivity = () => { const now = Date.now(); // Throttling: max. 1x pro Sekunde aktualisieren if (now - lastEventTime > 1000) { lastEventTime = now; // Wenn Modal bereits offen ist, Aktivität nicht im Hintergrund zurücksetzen if (!showModal) { updateLastActivityCookie(); } } }; const events = ["mousemove", "keydown", "click", "scroll", "touchstart"]; events.forEach((event) => { window.addEventListener(event, handleUserActivity, { passive: true }); }); return () => { events.forEach((event) => { window.removeEventListener(event, handleUserActivity); }); }; }, [isLoggedIn, showModal, updateLastActivityCookie]); // Countdown-Timer verwalten, wenn Modal geöffnet wird useEffect(() => { if (!showModal) { if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); } return; } countdownIntervalRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { clearInterval(countdownIntervalRef.current!); handleLogout("inactivity"); return 0; } return prev - 1; }); }, 1000); return () => { if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); } }; }, [showModal, handleLogout]); // Inaktivität zurücksetzen / Modal schließen const handleStayLoggedIn = () => { updateLastActivityCookie(); setShowModal(false); if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); } }; if (!isLoggedIn) return null; // Werte für den radialen SVG-Timer (Kreisumfang) const radius = 40; const circumference = 2 * Math.PI * radius; const strokeDashoffset = circumference - (countdown / COUNTDOWN_SECONDS) * circumference; return ( { if (!open) handleStayLoggedIn(); }}>
Noch da? Sie sind seit längerem inaktiv. Aus Sicherheitsgründen werden Sie gleich abgemeldet.
{/* Radialer SVG-Timer */}
{/* Hintergrundkreis */} {/* Countdown-Fortschrittskreis */} {/* Numerische Restzeit in Sekunden */}
{countdown}s Verbleibend
); }