diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx index a2faf186..a68651a4 100644 --- a/frontend/src/contexts/auth.context.tsx +++ b/frontend/src/contexts/auth.context.tsx @@ -21,6 +21,11 @@ import { Progress } from "@/app-components/displays/Progress"; import { useLogout } from "@/hooks/entities/auth-hooks"; import { useApiClient } from "@/hooks/useApiClient"; import { CURRENT_USER_KEY, PUBLIC_PATHS } from "@/hooks/useAuth"; +import { + EBCEvent, + ETabMode, + useBroadcastChannel, +} from "@/hooks/useBroadcastChannel"; import { useTranslate } from "@/hooks/useTranslate"; import { RouterType } from "@/services/types"; import { IUser } from "@/types/user.types"; @@ -59,6 +64,7 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { i18n.changeLanguage(lang); }; const { mutate: logoutSession } = useLogout(); + const { mode, value } = useBroadcastChannel(); const logout = async () => { updateLanguage(publicRuntimeConfig.lang.default); logoutSession(); @@ -107,6 +113,12 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { setIsReady(true); }, []); + useEffect(() => { + if (value === EBCEvent.LOGOUT_END_SESSION && mode === ETabMode.SECONDARY) { + router.reload(); + } + }, [value, mode, router]); + if (!isReady || isLoading) return ; return ( diff --git a/frontend/src/hooks/entities/auth-hooks.ts b/frontend/src/hooks/entities/auth-hooks.ts index e3a1b07b..fb78fed4 100755 --- a/frontend/src/hooks/entities/auth-hooks.ts +++ b/frontend/src/hooks/entities/auth-hooks.ts @@ -22,6 +22,7 @@ import { useSocket } from "@/websocket/socket-hooks"; import { useFind } from "../crud/useFind"; import { useApiClient } from "../useApiClient"; import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth"; +import { EBCEvent, useBroadcastChannel } from "../useBroadcastChannel"; import { useToast } from "../useToast"; import { useTranslate } from "../useTranslate"; @@ -58,6 +59,7 @@ export const useLogout = ( const { logoutRedirection } = useLogoutRedirection(); const { toast } = useToast(); const { t } = useTranslate(); + const { send } = useBroadcastChannel(); return useMutation({ ...options, @@ -68,6 +70,7 @@ export const useLogout = ( }, onSuccess: async () => { queryClient.removeQueries([CURRENT_USER_KEY]); + send(EBCEvent.LOGOUT_END_SESSION); await logoutRedirection(); toast.success(t("message.logout_success")); }, diff --git a/frontend/src/hooks/useBroadcastChannel.ts b/frontend/src/hooks/useBroadcastChannel.ts new file mode 100644 index 00000000..beba882d --- /dev/null +++ b/frontend/src/hooks/useBroadcastChannel.ts @@ -0,0 +1,57 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { useEffect, useRef, useState } from "react"; + +export enum ETabMode { + PRIMARY = "primary", + SECONDARY = "secondary", +} + +export enum EBCEvent { + LOGOUT_END_SESSION = "logout-end-session", +} + +export const useBroadcastChannel = ( + channelName: string = "main-broadcast-channel", + initialValue?: EBCEvent, +) => { + const channelRef = useRef(null); + const [value, setValue] = useState(initialValue); + const [mode, setMode] = useState(ETabMode.PRIMARY); + + useEffect(() => { + channelRef.current = new BroadcastChannel(channelName); + }, [channelName]); + + useEffect(() => { + if (channelRef.current) { + channelRef.current.addEventListener("message", (event) => { + if (mode === ETabMode.PRIMARY) { + setValue(event.data); + setMode(ETabMode.SECONDARY); + } + }); + channelRef.current.postMessage(initialValue); + } + + return () => { + if (channelRef.current?.onmessage) { + channelRef.current.onmessage = null; + } + channelRef.current?.close(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [channelName, initialValue]); + + const send = (data: EBCEvent) => { + channelRef.current?.postMessage(data); + }; + + return { mode, value, send }; +};