342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
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<string>) => {
|
|
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;
|