diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 19b88e53..ba2596be 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1,5 +1,20 @@ // API INDEX const BASE = (import.meta.env.VITE_API_URL || '').replace(/\/$/, ''); +export const COOKIE_MISSING_EVENT = 'bcan:cookie-missing'; +let hasDispatchedCookieMissingEvent = false; + +function notifyCookieMissing(path: string): void { + if (hasDispatchedCookieMissingEvent || typeof window === 'undefined') { + return; + } + + hasDispatchedCookieMissingEvent = true; + window.dispatchEvent( + new CustomEvent(COOKIE_MISSING_EVENT, { + detail: { path }, + }) + ); +} type ApiInit = RequestInit & { __retry?: boolean }; let refreshInFlight: Promise | null = null; @@ -31,6 +46,16 @@ export async function api( const cleanPath = path.startsWith('/') ? path : `/${path}`; const url = `${BASE}${cleanPath}`; + const response = await fetch(url, { + credentials: 'include', + ...init, + }); + + if (response.status === 401) { + notifyCookieMissing(cleanPath); + } + + return response; const typedInit = init as ApiInit; const { __retry, ...fetchInit } = typedInit; diff --git a/frontend/src/context/auth/authContext.tsx b/frontend/src/context/auth/authContext.tsx index 82cd7eb9..2cbaed6b 100644 --- a/frontend/src/context/auth/authContext.tsx +++ b/frontend/src/context/auth/authContext.tsx @@ -1,10 +1,11 @@ -import { useContext, createContext, ReactNode, useEffect, useRef } from 'react'; +import { useContext, createContext, ReactNode, useEffect, useRef, useState } from 'react'; import { getAppStore } from '../../external/bcanSatchel/store'; import { setAuthState, logoutUser } from '../../external/bcanSatchel/actions'; import { observer } from 'mobx-react-lite'; import { User } from '../../../../middle-layer/types/User'; -import { api } from '../../api'; +import { api, COOKIE_MISSING_EVENT } from '../../api'; import { fetchUsers } from '../../main-page/users/UserActions.ts'; +import Button from '../../components/Button'; /** @@ -32,6 +33,7 @@ export const useAuthContext = () => { export const AuthProvider = observer(({ children }: { children: ReactNode }) => { const store = getAppStore(); const logoutTimerRef = useRef | null>(null); + const [showCookieErrorPrompt, setShowCookieErrorPrompt] = useState(false); // Auto-logout timeout duration (in milliseconds) // 8 hours = 8 * 60 * 60 * 1000 const SESSION_TIMEOUT = 8 * 60 * 60 * 1000; @@ -135,6 +137,26 @@ export const AuthProvider = observer(({ children }: { children: ReactNode }) => }; }, [store.isAuthenticated]); + useEffect(() => { + const onCookieMissing = () => { + if (store.isAuthenticated) { + setShowCookieErrorPrompt(true); + } + }; + + window.addEventListener(COOKIE_MISSING_EVENT, onCookieMissing); + + return () => { + window.removeEventListener(COOKIE_MISSING_EVENT, onCookieMissing); + }; + }, [store.isAuthenticated]); + + useEffect(() => { + if (!store.isAuthenticated && showCookieErrorPrompt) { + setShowCookieErrorPrompt(false); + } + }, [showCookieErrorPrompt, store.isAuthenticated]); + /** Restore user session on refresh */ // useEffect(() => { // api('/auth/session') @@ -154,6 +176,22 @@ export const AuthProvider = observer(({ children }: { children: ReactNode }) => }} > {children} + {showCookieErrorPrompt && ( +
+
+

Internal Error

+

+ An internal error occurred and your session could not be verified. + Please log out and log back in. +

+
+
+ )} ); });