import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; // Define the initial state for the authentication slice interface AuthState { isLoggedIn: boolean; authMessage: string; status: "idle" | "loading" | "succeeded" | "failed"; error: string | null; user: { email: string; role: string; } | null; tokenInfo: { expiresAt: string; timeUntilExpiration: number; expiresInHours: number; } | null; } const initialState: AuthState = { isLoggedIn: false, authMessage: "", status: "idle", error: null, user: null, tokenInfo: null, }; // Async Thunk for Login // This handles the API call to your Next.js login Route Handler export const login = createAsyncThunk( "auth/login", async ( { email, password }: { email: string; password: string }, { rejectWithValue } ) => { try { const response = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email, password }), }); const data = await response.json(); if (!response.ok) { // If the server responded with an error status (e.g., 401, 400, 500) return rejectWithValue(data.message || "Login failed"); } // On successful login, the backend sets the HTTP-only cookie. // We'll set a client-side flag (like localStorage) for immediate UI updates, // though the primary source of truth for auth is the HTTP-only cookie. if (typeof window !== "undefined") { // Ensure localStorage access is client-side localStorage.setItem("userToken", "mock-authenticated"); // For client-side state sync } // After successful login, check auth status to get token information // This ensures we have the token expiration details immediately const authStatusResponse = await fetch("/api/auth/status"); if (authStatusResponse.ok) { const authData = await authStatusResponse.json(); return { message: data.message || "Login successful", user: authData.user, tokenInfo: authData.tokenInfo, }; } return data.message || "Login successful"; } catch (error: unknown) { // Handle network errors or other unexpected issues const errorMessage = error instanceof Error ? error.message : "Network error during login"; return rejectWithValue(errorMessage); } } ); // Async Thunk for Logout // This handles the API call to your Next.js logout Route Handler export const logout = createAsyncThunk( "auth/logout", async (_, { rejectWithValue }) => { try { const response = await fetch("/api/auth/logout", { method: "DELETE", }); const data = await response.json(); if (!response.ok) { // If the server responded with an error status return rejectWithValue(data.message || "Logout failed"); } if (typeof window !== "undefined") { // Ensure localStorage access is client-side localStorage.removeItem("userToken"); // Clear client-side flag } // After successful logout, check auth status to ensure proper cleanup const authStatusResponse = await fetch("/api/auth/status"); if (authStatusResponse.ok) { const authData = await authStatusResponse.json(); return { message: data.message || "Logged out successfully", isAuthenticated: authData.isAuthenticated, }; } return data.message || "Logged out successfully"; } catch (error: unknown) { // Handle network errors const errorMessage = error instanceof Error ? error.message : "Network error during logout"; return rejectWithValue(errorMessage); } } ); // Async Thunk for automatic logout (when token expires) export const autoLogout = createAsyncThunk( "auth/autoLogout", async (reason: string, { rejectWithValue }) => { try { // Clear the cookie by calling logout endpoint await fetch("/api/auth/logout", { method: "DELETE", }); if (typeof window !== "undefined") { // Clear client-side storage localStorage.removeItem("userToken"); } return { message: `Auto-logout: ${reason}` }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Auto-logout failed"; return rejectWithValue(errorMessage); } } ); // Async Thunk for manual refresh of authentication status export const refreshAuthStatus = createAsyncThunk( "auth/refreshStatus", async (_, { rejectWithValue }) => { try { const response = await fetch("/api/auth/status"); const data = await response.json(); if (!response.ok) { return rejectWithValue(data.message || "Authentication refresh failed"); } return data; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Network error during auth refresh"; return rejectWithValue(errorMessage); } } ); // Async Thunk for checking authentication status export const checkAuthStatus = createAsyncThunk( "auth/checkStatus", async (_, { rejectWithValue }) => { try { const response = await fetch("/api/auth/status"); const data = await response.json(); if (!response.ok) { return rejectWithValue(data.message || "Authentication check failed"); } return data; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Network error during auth check"; return rejectWithValue(errorMessage); } } ); // Create the authentication slice const authSlice = createSlice({ name: "auth", initialState, reducers: { // Reducer to set an authentication message (e.g., from UI actions) setAuthMessage: (state, action: PayloadAction) => { state.authMessage = action.payload; }, // Reducer to clear the authentication message clearAuthMessage: (state) => { state.authMessage = ""; }, // Reducer to initialize login status from client-side storage (e.g., on app load) // This is useful for cases where middleware might not redirect immediately, // or for client-side rendering of protected content based on initial state. initializeAuth: (state) => { if (typeof window !== "undefined") { // Ensure this runs only on the client const userToken = localStorage.getItem("userToken"); state.isLoggedIn = userToken === "mock-authenticated"; } }, }, extraReducers: (builder) => { builder // Login Thunk Reducers .addCase(login.pending, (state) => { state.status = "loading"; state.error = null; state.authMessage = "Attempting login..."; }) .addCase(login.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = true; // Handle both old string payload and new object payload if (typeof action.payload === "string") { state.authMessage = action.payload; } else { state.authMessage = action.payload.message; state.user = action.payload.user || null; state.tokenInfo = action.payload.tokenInfo || null; } }) .addCase(login.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.error = action.payload as string; state.authMessage = action.payload as string; // Display error message }) // Logout Thunk Reducers .addCase(logout.pending, (state) => { state.status = "loading"; state.error = null; state.authMessage = "Logging out..."; }) .addCase(logout.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = false; // Handle both old string payload and new object payload if (typeof action.payload === "string") { state.authMessage = action.payload; } else { state.authMessage = action.payload.message; // Ensure we're properly logged out if (action.payload.isAuthenticated === false) { state.user = null; state.tokenInfo = null; } } // Always clear user data on logout state.user = null; state.tokenInfo = null; }) .addCase(logout.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = true; // Stay logged in if logout failed state.error = action.payload as string; state.authMessage = action.payload as string; // Display error message }) // Check Auth Status Thunk Reducers .addCase(checkAuthStatus.pending, (state) => { state.status = "loading"; state.error = null; }) .addCase(checkAuthStatus.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = action.payload.isAuthenticated; state.user = action.payload.user || null; state.tokenInfo = action.payload.tokenInfo || null; state.error = null; }) .addCase(checkAuthStatus.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.user = null; state.tokenInfo = null; state.error = action.payload as string; }) // Auto Logout Thunk Reducers .addCase(autoLogout.pending, (state) => { state.status = "loading"; state.error = null; state.authMessage = "Session expired, logging out..."; }) .addCase(autoLogout.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = false; state.authMessage = action.payload.message; state.user = null; state.tokenInfo = null; state.error = null; }) .addCase(autoLogout.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.user = null; state.tokenInfo = null; state.error = action.payload as string; state.authMessage = "Auto-logout failed"; }) // Refresh Auth Status Thunk Reducers .addCase(refreshAuthStatus.pending, (state) => { state.status = "loading"; state.error = null; }) .addCase(refreshAuthStatus.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = action.payload.isAuthenticated; state.user = action.payload.user || null; state.tokenInfo = action.payload.tokenInfo || null; state.error = null; }) .addCase(refreshAuthStatus.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.user = null; state.tokenInfo = null; state.error = action.payload as string; }); }, }); export const { setAuthMessage, clearAuthMessage, initializeAuth } = authSlice.actions; export default authSlice.reducer;