From 62e3c7804aa383f765c97adffcde394d6974ede0 Mon Sep 17 00:00:00 2001 From: DanielS Date: Wed, 24 Jun 2026 00:15:59 +0200 Subject: [PATCH] feat: fix logout button, inactivity tracker, and concurrent session termination --- shop/app/admin/layout.tsx | 7 +- shop/app/layout.tsx | 2 + shop/components/NavbarClient.tsx | 6 ++ shop/components/admin/logout-menu-item.tsx | 62 ++++++++++++++ shop/components/inactivity-tracker.tsx | 98 ++++++++++++++++++++++ shop/lib/actions/auth.ts | 3 + 6 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 shop/components/admin/logout-menu-item.tsx create mode 100644 shop/components/inactivity-tracker.tsx diff --git a/shop/app/admin/layout.tsx b/shop/app/admin/layout.tsx index 82dfd0c..b64c7ae 100644 --- a/shop/app/admin/layout.tsx +++ b/shop/app/admin/layout.tsx @@ -1,8 +1,8 @@ import Link from 'next/link' import { LayoutDashboard, Package, Settings, Users, LogOut, LayoutGrid, ShoppingCart } from 'lucide-react' - import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' +import { AdminLogoutButton } from '@/components/admin/logout-menu-item' export default async function AdminLayout({ children, @@ -64,10 +64,7 @@ export default async function AdminLayout({
- +
CASPOS Store diff --git a/shop/app/layout.tsx b/shop/app/layout.tsx index 9e03b7b..8c5172d 100644 --- a/shop/app/layout.tsx +++ b/shop/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist } from "next/font/google"; import { ThemeProvider } from "next-themes"; +import { InactivityTracker } from "@/components/inactivity-tracker"; import "./globals.css"; const defaultUrl = process.env.VERCEL_URL @@ -33,6 +34,7 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > + {children} diff --git a/shop/components/NavbarClient.tsx b/shop/components/NavbarClient.tsx index 0e50db6..d0c9b47 100644 --- a/shop/components/NavbarClient.tsx +++ b/shop/components/NavbarClient.tsx @@ -7,6 +7,7 @@ import { createBrowserClient } from "@supabase/ssr"; import { type User } from "@supabase/supabase-js"; import { Rocket, Menu, X, User as UserIcon, LogOut } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { signOut } from "@/lib/actions/auth"; interface NavbarClientProps { user: User | null; @@ -93,6 +94,11 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) { }, []); const handleSignOut = async () => { + try { + await signOut(); + } catch (e) { + console.error("Fehler beim Server-Signout:", e); + } await supabase.auth.signOut(); setCurrentUser(null); setIsMobileMenuOpen(false); diff --git a/shop/components/admin/logout-menu-item.tsx b/shop/components/admin/logout-menu-item.tsx new file mode 100644 index 0000000..446236c --- /dev/null +++ b/shop/components/admin/logout-menu-item.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { signOut } from "@/lib/actions/auth"; +import { useRouter } from "next/navigation"; +import { LogOut } from "lucide-react"; +import { createBrowserClient } from "@supabase/ssr"; + +export function AdminLogoutButton() { + const router = useRouter(); + + // 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; + + // Supabase URL Auflösung inline + let resolvedUrl = supabaseUrl; + if (resolvedUrl && typeof window !== "undefined") { + try { + const parsed = new URL(resolvedUrl); + if (parsed.hostname === "supabase-kong" || parsed.hostname === "kong") { + parsed.hostname = window.location.hostname; + resolvedUrl = parsed.toString().replace(/\/$/, ""); + } + } catch { + resolvedUrl = resolvedUrl + .replace("//supabase-kong", `//${window.location.hostname}`) + .replace("//kong", `//${window.location.hostname}`); + } + } + + // Browser Client inline erstellen + const supabase = createBrowserClient( + resolvedUrl!, + supabaseAnonKey!, + { + cookieOptions: { + name: "webshop-auth-token", + }, + } + ); + + const handleLogout = async () => { + try { + await signOut(); + } catch (error) { + console.error("Fehler beim Abmelden:", error); + } + await supabase.auth.signOut(); + router.push("/"); + router.refresh(); + }; + + return ( + + ); +} diff --git a/shop/components/inactivity-tracker.tsx b/shop/components/inactivity-tracker.tsx new file mode 100644 index 0000000..65f7f72 --- /dev/null +++ b/shop/components/inactivity-tracker.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useEffect } from "react"; +import { createBrowserClient } from "@supabase/ssr"; +import { useRouter } from "next/navigation"; +import { signOut } from "@/lib/actions/auth"; + +export function InactivityTracker() { + const router = useRouter(); + + // 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; + + // Supabase URL Auflösung inline + let resolvedUrl = supabaseUrl; + if (resolvedUrl && typeof window !== "undefined") { + try { + const parsed = new URL(resolvedUrl); + if (parsed.hostname === "supabase-kong" || parsed.hostname === "kong") { + parsed.hostname = window.location.hostname; + resolvedUrl = parsed.toString().replace(/\/$/, ""); + } + } catch { + resolvedUrl = resolvedUrl + .replace("//supabase-kong", `//${window.location.hostname}`) + .replace("//kong", `//${window.location.hostname}`); + } + } + + // Browser Client inline erstellen + const supabase = createBrowserClient( + resolvedUrl!, + supabaseAnonKey!, + { + cookieOptions: { + name: "webshop-auth-token", + }, + } + ); + + useEffect(() => { + const INACTIVITY_LIMIT = 5 * 60 * 1000; // 5 Minuten in ms + + const updateActivity = () => { + localStorage.setItem("last_activity", Date.now().toString()); + }; + + // Events zum Tracken der Benutzerinteraktion + const events = ["mousemove", "keydown", "mousedown", "touchstart", "scroll", "click"]; + events.forEach((event) => window.addEventListener(event, updateActivity)); + + // Aktivitätszeitstempel beim Mounten initialisieren + updateActivity(); + + // Regelmäßige Überprüfung der Inaktivität + const interval = setInterval(async () => { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) return; // Nicht eingeloggt, keine Prüfung nötig + + // Prüfen, ob die Session auf dem Server noch gültig ist (z.B. wegen Login auf anderem Browser) + const { data: { user }, error } = await supabase.auth.getUser(); + if (error || !user) { + try { + await signOut(); + } catch (e) { + console.error("Fehler beim Inactivity-Server-Signout:", e); + } + await supabase.auth.signOut(); + localStorage.removeItem("last_activity"); + router.push("/"); + router.refresh(); + return; + } + + const lastActivity = Number(localStorage.getItem("last_activity") || Date.now()); + if (Date.now() - lastActivity > INACTIVITY_LIMIT) { + // Bei Überschreitung abmelden + try { + await signOut(); + } catch (e) { + console.error("Fehler beim Inactivity-Server-Signout:", e); + } + await supabase.auth.signOut(); + localStorage.removeItem("last_activity"); + router.push("/"); + router.refresh(); + } + }, 5000); // Prüfung alle 5 Sekunden + + return () => { + events.forEach((event) => window.removeEventListener(event, updateActivity)); + clearInterval(interval); + }; + }, [supabase, router]); + + return null; +} diff --git a/shop/lib/actions/auth.ts b/shop/lib/actions/auth.ts index 545d49d..cc7ae0b 100644 --- a/shop/lib/actions/auth.ts +++ b/shop/lib/actions/auth.ts @@ -25,6 +25,9 @@ export async function signIn(email: string, password: string) { await supabase.auth.signOut() throw new Error("Zugriff verweigert. Nur registrierte Partner dürfen sich anmelden.") } + + // Andere aktive Sitzungen beenden + await supabase.auth.signOut({ scope: 'others' }) } return { success: true }