Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pkg/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
}

Expand Down
2 changes: 2 additions & 0 deletions resources/js/Layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -19,6 +20,7 @@ export default function AppLayout({
const { flash } = usePage<SharedProps>().props;

useFlashToasts(flash);
useAuthGuard();

return (
<AppLayoutTemplate breadcrumbs={breadcrumbs} {...props}>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/Layouts/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,6 +19,7 @@ export default function AuthLayout({
const { flash } = usePage<SharedProps>().props;

useFlashToasts(flash);
useAuthGuard();

return (
<AuthLayoutTemplate title={title} description={description} {...props}>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/Layouts/PublicLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SharedProps>().props;

useFlashToasts(flash);
useAuthGuard();

return (
<div className="min-h-screen text-foreground flex flex-col">
Expand Down
60 changes: 60 additions & 0 deletions resources/js/hooks/useAuthGuard.ts
Original file line number Diff line number Diff line change
@@ -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<SharedProps>().props;
const currentUserId = auth?.user?.id ?? null;
const initialUserIdRef = useRef<number | null>(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]);
}
Loading