feat: fix logout button, inactivity tracker, and concurrent session termination
All checks were successful
Staging Build / build (push) Successful in 2m16s
All checks were successful
Staging Build / build (push) Successful in 2m16s
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { LayoutDashboard, Package, Settings, Users, LogOut, LayoutGrid, ShoppingCart } from 'lucide-react'
|
import { LayoutDashboard, Package, Settings, Users, LogOut, LayoutGrid, ShoppingCart } from 'lucide-react'
|
||||||
|
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
import { AdminLogoutButton } from '@/components/admin/logout-menu-item'
|
||||||
|
|
||||||
export default async function AdminLayout({
|
export default async function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -64,10 +64,7 @@ export default async function AdminLayout({
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="p-4 border-t border-white/5 space-y-4">
|
<div className="p-4 border-t border-white/5 space-y-4">
|
||||||
<button className="flex w-full items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
|
<AdminLogoutButton />
|
||||||
<LogOut className="w-5 h-5" />
|
|
||||||
Abmelden
|
|
||||||
</button>
|
|
||||||
<div className="px-3 pt-2 flex items-center gap-2 border-t border-white/5 opacity-60">
|
<div className="px-3 pt-2 flex items-center gap-2 border-t border-white/5 opacity-60">
|
||||||
<span className="font-bold text-sm tracking-tighter text-white">CASPOS Store</span>
|
<span className="font-bold text-sm tracking-tighter text-white">CASPOS Store</span>
|
||||||
<span className="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full font-semibold uppercase tracking-wider border border-amber-500/30">
|
<span className="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full font-semibold uppercase tracking-wider border border-amber-500/30">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist } from "next/font/google";
|
import { Geist } from "next/font/google";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import { InactivityTracker } from "@/components/inactivity-tracker";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const defaultUrl = process.env.VERCEL_URL
|
const defaultUrl = process.env.VERCEL_URL
|
||||||
@@ -33,6 +34,7 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
|
<InactivityTracker />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { createBrowserClient } from "@supabase/ssr";
|
|||||||
import { type User } from "@supabase/supabase-js";
|
import { type User } from "@supabase/supabase-js";
|
||||||
import { Rocket, Menu, X, User as UserIcon, LogOut } from "lucide-react";
|
import { Rocket, Menu, X, User as UserIcon, LogOut } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { signOut } from "@/lib/actions/auth";
|
||||||
|
|
||||||
interface NavbarClientProps {
|
interface NavbarClientProps {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
@@ -93,6 +94,11 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await signOut();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Fehler beim Server-Signout:", e);
|
||||||
|
}
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
setCurrentUser(null);
|
setCurrentUser(null);
|
||||||
setIsMobileMenuOpen(false);
|
setIsMobileMenuOpen(false);
|
||||||
|
|||||||
62
shop/components/admin/logout-menu-item.tsx
Normal file
62
shop/components/admin/logout-menu-item.tsx
Normal file
@@ -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 (
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex w-full items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all text-left"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
Abmelden
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
shop/components/inactivity-tracker.tsx
Normal file
98
shop/components/inactivity-tracker.tsx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ export async function signIn(email: string, password: string) {
|
|||||||
await supabase.auth.signOut()
|
await supabase.auth.signOut()
|
||||||
throw new Error("Zugriff verweigert. Nur registrierte Partner dürfen sich anmelden.")
|
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 }
|
return { success: true }
|
||||||
|
|||||||
Reference in New Issue
Block a user