diff --git a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx index 1c342cc..b0839ba 100644 --- a/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx +++ b/webapp/src/app/[locale]/(GlobalWrapper)/(Auth)/layout.tsx @@ -1,62 +1,110 @@ 'use client'; import { useAppSelector } from "@/redux/store" -import { isLoggedIn , mountCheckIfValid } from "@/redux/features/auth-slice"; +import { isLoggedIn, mountCheckIfValid, logOut } from "@/redux/features/auth-slice"; import { useDispatch } from "react-redux" import { AppDispatch } from '@/redux/store'; -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; import { useRouter } from 'next/navigation' import { ReactNode } from 'react' import FullScreenLoader from "@/components/common/fullScreenLoader"; import { load } from '@/redux/features/settings-slice' +import ErrorBoundary from '@/components/common/ErrorBoundary'; +import axios from 'axios'; // Added axios import -// HOC for auth pages ( only accept auth user ) -export default function LocaleLayout({children} : { children : ReactNode }) { - - const router = useRouter() +// HOC for auth pages (only accept auth user) +export default function LocaleLayout({ children }: { children: ReactNode }) { + const router = useRouter(); const dispatch = useDispatch(); - // load states - const isValid = useAppSelector((state) => state.authReducer.value.isValid) - const checkIfValidMounted = useAppSelector((state) => state.authReducer.value.checkIfValidMounted) - const notAuthRedirectPage = useAppSelector((state) => state.settingsReducer.value.notAuthRedirectPage) - const isLoadingSettings = useAppSelector((state) => state.settingsReducer.value.isLoadingSettings) - const loadedFirstTime = useAppSelector((state) => state.settingsReducer.value.loadedFirstTime) - // Get redux states - // init isLoggedIn + + // Load states + const isValid = useAppSelector((state) => state.authReducer.value.isValid); + const checkIfValidMounted = useAppSelector((state) => state.authReducer.value.checkIfValidMounted); + const notAuthRedirectPage = useAppSelector((state) => state.settingsReducer.value.notAuthRedirectPage); + const isLoadingSettings = useAppSelector((state) => state.settingsReducer.value.isLoadingSettings); + const loadedFirstTime = useAppSelector((state) => state.settingsReducer.value.loadedFirstTime); + + // Handle not logged in state + const handleNotLoggedIn = useCallback(() => { + // Clear any invalid token from cookies + document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + // Clear Redux state + dispatch(logOut()); + // Redirect to login page + router.push(notAuthRedirectPage); + }, [notAuthRedirectPage, router, dispatch]); + + // Initialize auth check useEffect(() => { - if(checkIfValidMounted) return - dispatch(mountCheckIfValid()) - async function a() - { - await dispatch(isLoggedIn({NotLoggedInCallback})) - } - a() - }, []) - // load settings - useEffect(() => { - if(loadedFirstTime) return - if(isLoadingSettings) return - async function a() - { - await dispatch(load({page : 1})) - } - a() - }, []) - // if wasnt logged in this will fire - function NotLoggedInCallback() - { - return router.push(notAuthRedirectPage) - } - return ( - <> - { - isValid ? - <> - {children} - - : - + if (checkIfValidMounted) return; + + const checkAuth = async () => { + try { + await dispatch(mountCheckIfValid()); + await dispatch(isLoggedIn({ NotLoggedInCallback: handleNotLoggedIn })); + } catch (error) { + console.error('Auth check failed:', error); + handleNotLoggedIn(); } - + }; + + checkAuth(); + }, [checkIfValidMounted, dispatch, handleNotLoggedIn]); + + // Load settings + useEffect(() => { + if (loadedFirstTime || isLoadingSettings) return; + + const loadSettings = async () => { + try { + await dispatch(load({ page: 1 })); + } catch (error) { + console.error('Failed to load settings:', error); + } + }; + + loadSettings(); + }, [loadedFirstTime, isLoadingSettings, dispatch]); + + // Add global error handler for API requests + useEffect(() => { + const responseInterceptor = (response: any) => response; + + const errorInterceptor = (error: any) => { + if (error.response?.status === 401) { + // Handle unauthorized (token expired/invalid) + handleNotLoggedIn(); + } + return Promise.reject(error); + }; + + // Add request interceptor + const requestInterceptor = axios.interceptors.request.use( + config => config, + error => Promise.reject(error) + ); + + // Add response interceptor + const responseIntercept = axios.interceptors.response.use( + responseInterceptor, + errorInterceptor + ); + + // Cleanup function + return () => { + axios.interceptors.request.eject(requestInterceptor); + axios.interceptors.response.eject(responseIntercept); + }; + }, [handleNotLoggedIn]); + + // Show loading state while checking auth + if (isValid === null) { + return ; + } + + return ( + + {isValid ? children : } + ); } diff --git a/webapp/src/components/common/ErrorBoundary.tsx b/webapp/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..4360be0 --- /dev/null +++ b/webapp/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Component, ErrorInfo, ReactNode } from 'react'; +import { useRouter } from 'next/navigation'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + private handleReset = () => { + // Clear auth token and reload + document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + window.location.href = '/'; + }; + + public render() { + if (this.state.hasError) { + return ( +
+
+

+ {this.state.error?.message === 'Session expired' ? 'Session Expired' : 'Something went wrong'} +

+

+ {this.state.error?.message === 'Session expired' + ? 'Your session has expired. Please log in again.' + : 'An unexpected error occurred. Please try again.'} +

+ +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/webapp/src/redux/features/auth-slice.ts b/webapp/src/redux/features/auth-slice.ts index 4e6269a..54fc484 100644 --- a/webapp/src/redux/features/auth-slice.ts +++ b/webapp/src/redux/features/auth-slice.ts @@ -5,7 +5,7 @@ * * https://redux-toolkit.js.org/api/createAsyncThunk */ -import { createAsyncThunk , createSlice } from '@reduxjs/toolkit' +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import axios from 'axios' import Cookies from 'universal-cookie'; import { fireAlert } from './alert-slice'; @@ -33,11 +33,10 @@ const initialState = { const logIn = createAsyncThunk( 'auth/logInStatus', - async (actionPayload : {username: string , password: string , successCallback : any}, thunkAPI) => { + async (actionPayload: { username: string, password: string, successCallback: any }, thunkAPI) => { try { - let { data } = await axios.post('/api/auth' , actionPayload) - if(data.success) - { + let { data } = await axios.post('/api/auth', actionPayload) + if (data.success) { actionPayload.successCallback() // fire the success alert thunkAPI.dispatch(fireAlert({ @@ -46,47 +45,78 @@ const logIn = createAsyncThunk( })) return data } - else - { + else { thunkAPI.dispatch(fireAlert({ success: false, message: data.message })) - return { success : false } + return { success: false } } - }catch(err) { + } catch (err) { thunkAPI.dispatch(fireAlert({ success: false, message: "unkownError", })) - return { success : false } + return { success: false } } } ) const isLoggedIn = createAsyncThunk( 'auth/isLoggedInStatus', - async (actionPayload : {LoggedInCallback? : any , NotLoggedInCallback? : any}, thunkAPI) => { + async (actionPayload: { LoggedInCallback?: () => void; NotLoggedInCallback?: () => void }, { dispatch, getState }) => { try { - const state : any = thunkAPI.getState() - let { data } = await axios.get('/api/auth?authToken='+state.authReducer.value.authToken) - if(!data.success && actionPayload.NotLoggedInCallback) - { - actionPayload.NotLoggedInCallback() + const state = getState() as RootState; + const { authToken } = state.authReducer.value; + + // If no token, immediately trigger not logged in + if (!authToken) { + if (actionPayload.NotLoggedInCallback) { + actionPayload.NotLoggedInCallback(); + } + return { isValid: false }; } - else if(actionPayload.LoggedInCallback && data.success) - { - actionPayload.LoggedInCallback() + + try { + const { data } = await axios.get('/api/auth?authToken=' + authToken); + + if (!data.success) { + // If token is invalid or expired, clear it + if (data.message === 'expiredToken' || data.message === 'invalidToken') { + dispatch(logOut()); + document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + } + + if (actionPayload.NotLoggedInCallback) { + actionPayload.NotLoggedInCallback(); + } + } else if (actionPayload.LoggedInCallback) { + actionPayload.LoggedInCallback(); + } + + return { isValid: data.success }; + } catch (error: any) { + // Handle network errors or server issues + console.error('Token validation error:', error); + + // If it's an auth-related error, clear the token + if (error.response?.status === 401 || error.response?.data?.message === 'expiredToken') { + dispatch(logOut()); + document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'; + } + + if (actionPayload.NotLoggedInCallback) { + actionPayload.NotLoggedInCallback(); + } + + return { isValid: false }; } - return { - isValid : data.success - } - }catch(e : any) - { - actionPayload.NotLoggedInCallback() - return { - isValid : false + } catch (error) { + console.error('Error in isLoggedIn:', error); + if (actionPayload.NotLoggedInCallback) { + actionPayload.NotLoggedInCallback(); } + return { isValid: false }; } } ) @@ -108,16 +138,15 @@ export const auth = createSlice({ }, extraReducers: (builder) => { // logIn thunk reducer - builder.addCase(logIn.fulfilled, (state : IinitialState , action) => { + builder.addCase(logIn.fulfilled, (state: IinitialState, action) => { // set the state - if(action.payload.success) - { + if (action.payload.success) { state.value.authToken = action.payload.authToken; state.value.isValid = true } }) // check if user authToken cookie is a valid one - builder.addCase(isLoggedIn.fulfilled, (state : IinitialState , action) => { + builder.addCase(isLoggedIn.fulfilled, (state: IinitialState, action) => { // set the state state.value.isValid = action.payload.isValid; state.value.checkIfValidMounted = true @@ -126,6 +155,6 @@ export const auth = createSlice({ }) // export the functions -export const { logOut , mountCheckIfValid } = auth.actions; -export { logIn , isLoggedIn }; +export const { logOut, mountCheckIfValid } = auth.actions; +export { logIn, isLoggedIn }; export default auth.reducer; \ No newline at end of file