From 84d13e6073790e910e613d6d224dca8717f7ad50 Mon Sep 17 00:00:00 2001 From: NicolasBernardes Date: Tue, 10 Mar 2026 19:15:42 -0300 Subject: [PATCH] fix: add cross-tab session protection and improve auth redirects - Add useAuthGuard hook with localStorage storage event for real-time cross-tab session change detection - Redirect unauthenticated users to login instead of returning 401 - Show warning message when authenticated users try to access login/register --- pkg/middleware/auth.go | 7 +++- resources/js/Layouts/AppLayout.tsx | 2 + resources/js/Layouts/AuthLayout.tsx | 2 + resources/js/Layouts/PublicLayout.tsx | 2 + resources/js/hooks/useAuthGuard.ts | 60 +++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 resources/js/hooks/useAuthGuard.ts diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 923483f..cc7f5fe 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -87,10 +87,12 @@ func LoadValidPasswordToken(authClient *services.AuthClient) echo.MiddlewareFunc } // RequireAuthentication requires that the user be authenticated in order to proceed. +// Unauthenticated users are redirected to the login page instead of receiving a 401 error. func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if u := c.Get(context.AuthenticatedUserKey); u == nil { - return echo.NewHTTPError(http.StatusUnauthorized) + msg.Warning(c, "Please log in to access this page.") + return c.Redirect(http.StatusSeeOther, c.Echo().Reverse(routenames.Login)) } return next(c) @@ -101,6 +103,9 @@ func RequireAuthentication(next echo.HandlerFunc) echo.HandlerFunc { func RequireNoAuthentication(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if u := c.Get(context.AuthenticatedUserKey); u != nil { + if user, ok := u.(*ent.User); ok { + msg.Warning(c, fmt.Sprintf("You are already logged in as %s. Please log out first to use another account.", user.Name)) + } return c.Redirect(http.StatusSeeOther, c.Echo().Reverse(routenames.Dashboard)) } diff --git a/resources/js/Layouts/AppLayout.tsx b/resources/js/Layouts/AppLayout.tsx index 3356d07..d176898 100644 --- a/resources/js/Layouts/AppLayout.tsx +++ b/resources/js/Layouts/AppLayout.tsx @@ -3,6 +3,7 @@ import AppLayoutTemplate from "@/Layouts/App/AppSidebarLayout"; import { type BreadcrumbItem } from "@/types"; import { type ReactNode } from "react"; import { useFlashToasts } from "@/hooks/useFlashToast"; +import { useAuthGuard } from "@/hooks/useAuthGuard"; import { SharedProps } from "@/types/global"; import { Toaster } from "sonner"; @@ -19,6 +20,7 @@ export default function AppLayout({ const { flash } = usePage().props; useFlashToasts(flash); + useAuthGuard(); return ( diff --git a/resources/js/Layouts/AuthLayout.tsx b/resources/js/Layouts/AuthLayout.tsx index ef1ea65..5759137 100644 --- a/resources/js/Layouts/AuthLayout.tsx +++ b/resources/js/Layouts/AuthLayout.tsx @@ -1,5 +1,6 @@ import AuthLayoutTemplate from "@/Layouts/Auth/AuthSimpleLayout"; import { useFlashToasts } from "@/hooks/useFlashToast"; +import { useAuthGuard } from "@/hooks/useAuthGuard"; import { Toaster } from "@/components/ui/sonner"; import { SharedProps } from "@/types/global"; import { usePage } from "@inertiajs/react"; @@ -18,6 +19,7 @@ export default function AuthLayout({ const { flash } = usePage().props; useFlashToasts(flash); + useAuthGuard(); return ( diff --git a/resources/js/Layouts/PublicLayout.tsx b/resources/js/Layouts/PublicLayout.tsx index 747548e..d517783 100644 --- a/resources/js/Layouts/PublicLayout.tsx +++ b/resources/js/Layouts/PublicLayout.tsx @@ -3,11 +3,13 @@ import { ReactNode } from "react"; import { Toaster } from "@/components/ui/sonner"; import { SharedProps } from "@/types/global"; import { useFlashToasts } from "@/hooks/useFlashToast"; +import { useAuthGuard } from "@/hooks/useAuthGuard"; export default function PublicLayout({ children }: { children: ReactNode }) { const { flash, auth } = usePage().props; useFlashToasts(flash); + useAuthGuard(); return (
diff --git a/resources/js/hooks/useAuthGuard.ts b/resources/js/hooks/useAuthGuard.ts new file mode 100644 index 0000000..6e57db9 --- /dev/null +++ b/resources/js/hooks/useAuthGuard.ts @@ -0,0 +1,60 @@ +import { usePage } from "@inertiajs/react"; +import { useEffect, useRef } from "react"; +import { SharedProps } from "@/types/global"; +import { toast } from "sonner"; + +const AUTH_STORAGE_KEY = "auth_user_id"; + +/** + * Detects when the authenticated user changes across browser tabs in real-time. + * + * How it works: + * - Each tab writes its current user ID to localStorage on login/navigation. + * - The `storage` event fires in OTHER tabs when localStorage changes. + * - When a tab detects a different user logged in or a logout, it redirects + * to the appropriate page (login if logged out, home if user switched). + */ +export function useAuthGuard() { + const { auth } = usePage().props; + const currentUserId = auth?.user?.id ?? null; + const initialUserIdRef = useRef(currentUserId); + + // Write current user ID to localStorage so other tabs can detect changes + useEffect(() => { + const value = currentUserId !== null ? String(currentUserId) : ""; + localStorage.setItem(AUTH_STORAGE_KEY, value); + }, [currentUserId]); + + // Listen for login/logout events from other tabs + useEffect(() => { + function onStorageChange(e: StorageEvent) { + if (e.key !== AUTH_STORAGE_KEY) return; + + const newId = e.newValue ? Number(e.newValue) : null; + const myId = initialUserIdRef.current; + + if (newId === myId) return; + + if (!newId) { + // Another tab logged out — redirect to login page + toast.warning("You have been logged out from another tab."); + setTimeout(() => (window.location.href = "/user/login"), 1000); + } else { + // Another tab logged in as a different user — redirect to home + toast.warning("Another account signed in. Redirecting..."); + setTimeout(() => (window.location.href = "/"), 1000); + } + } + + window.addEventListener("storage", onStorageChange); + return () => window.removeEventListener("storage", onStorageChange); + }, []); + + // Also detect user changes from Inertia responses (same-tab safety net) + useEffect(() => { + if (initialUserIdRef.current !== currentUserId) { + initialUserIdRef.current = currentUserId; + window.location.href = currentUserId ? "/" : "/user/login"; + } + }, [currentUserId]); +}