import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import { ThunkSuccess, ThunkError, IUserResponse } from "../types"; import { IEditUserForm } from "../../features/UserRoles/User.interfaces"; import { RootState } from "../store"; import toast from "react-hot-toast"; interface TokenInfo { expiresAt: string; timeUntilExpiration: number; expiresInHours: number; } interface AuthState { isLoggedIn: boolean; authMessage: string; status: "idle" | "loading" | "succeeded" | "failed"; error: string | null; user: IUserResponse | null; tokenInfo: TokenInfo | null; mustChangePassword: boolean; addedUser: IUserResponse | null; } const initialState: AuthState = { isLoggedIn: false, authMessage: "", status: "idle", error: null, user: null, tokenInfo: null, mustChangePassword: false, addedUser: null, }; // ---------------- Login ---------------- export const login = createAsyncThunk< ThunkSuccess<{ user: IUserResponse | null; tokenInfo: TokenInfo | null; mustChangePassword: boolean; }>, { email: string; password: string }, { rejectValue: ThunkError } >( "auth/login", async ( { email, password }: { email: string; password: string }, { rejectWithValue } ) => { try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); const data = await res.json(); if (!res.ok) { toast.error(data.message || "Login failed"); return rejectWithValue((data.message as string) || "Login failed"); } // After login, validate session right away const validateRes = await fetch("/api/auth/validate", { method: "POST" }); const validateData = await validateRes.json(); toast.success(data.message || "Login successful"); return { message: data.message || "Login successful", user: data.user || null, tokenInfo: validateData.tokenInfo || null, mustChangePassword: data.must_change_password || false, } as ThunkSuccess<{ user: IUserResponse | null; tokenInfo: TokenInfo | null; mustChangePassword: boolean; }>; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during login" ); } } ); // ---------------- Logout ---------------- export const logout = createAsyncThunk< ThunkSuccess, void, { rejectValue: ThunkError } >("auth/logout", async (_, { rejectWithValue }) => { try { const res = await fetch("/api/auth/logout", { method: "DELETE" }); const data = await res.json(); if (!res.ok) return rejectWithValue((data.message as string) || "Logout failed"); // Persisted state will be cleared by reducers below; avoid direct persistor usage here. return { message: data.message || "Logged out successfully" }; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during logout" ); } }); // ---------------- Auto Logout ---------------- export const autoLogout = createAsyncThunk< ThunkSuccess, string, { rejectValue: ThunkError } >("auth/autoLogout", async (reason: string, { rejectWithValue }) => { try { await fetch("/api/auth/logout", { method: "DELETE" }); return { message: `Auto-logout: ${reason}` }; } catch (err: unknown) { return rejectWithValue((err as Error).message || "Auto-logout failed"); } }); // ---------------- Change Password ---------------- export const changePassword = createAsyncThunk< ThunkSuccess<{ success: boolean }>, { email: string; newPassword: string; currentPassword?: string }, { rejectValue: ThunkError } >( "auth/changePassword", async ({ email, newPassword, currentPassword }, { rejectWithValue }) => { try { const res = await fetch("/api/auth/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, newPassword, currentPassword }), }); const data = await res.json(); if (!res.ok) return rejectWithValue( (data.message as string) || "Password change failed" ); return { success: data.success, message: data.message || "Password changed successfully", }; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during password change" ); } } ); // ---------------- Unified Validate Auth ---------------- export const validateAuth = createAsyncThunk< { tokenInfo: TokenInfo | null }, void, { rejectValue: ThunkError } >("auth/validate", async (_, { rejectWithValue, dispatch }) => { try { const res = await fetch("/api/auth/validate", { method: "POST" }); const data = await res.json(); if (!res.ok || !data.success) { toast.error(data.message || "Session invalid or expired"); dispatch(autoLogout("Session invalid or expired")); return rejectWithValue("Session invalid or expired"); } return { tokenInfo: data.tokenInfo || null, }; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during validation" ); } }); // TODO - Creaye a new thunk to update the user stuff // ---------------- Add User ---------------- export const addUser = createAsyncThunk< ThunkSuccess<{ user: IUserResponse; success: boolean }>, IEditUserForm, { rejectValue: ThunkError; state: RootState } >("auth/addUser", async (userData, { rejectWithValue, getState }) => { try { const state = getState(); const currentUserId = state.auth.user?.id; console.log("[DEBUG] [ADD-USER] [currentUserId]: ", currentUserId); const res = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...userData, creator: currentUserId || "" }), }); const data = await res.json(); console.log("[DEBUG] [ADD-USER] [data]: ", data); if (!res.ok) { return rejectWithValue(data.message || "Failed to create user"); } return { success: data.success, message: data.message || "User created successfully", user: data.user, } as ThunkSuccess<{ user: IUserResponse; success: boolean }>; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during user creation" ); } }); // ---------------- Update User Details ---------------- export const updateUserDetails = createAsyncThunk< ThunkSuccess<{ user: IUserResponse }>, { id: string; updates: Partial }, { rejectValue: ThunkError } >("auth/updateUserDetails", async ({ id, updates }, { rejectWithValue }) => { try { const res = await fetch(`/api/dashboard/admin/users/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify(updates), }); const data = await res.json(); if (!res.ok) { return rejectWithValue(data.message || "Failed to update user details"); } return { message: data.message || "User details updated successfully", user: data.user, }; } catch (err: unknown) { return rejectWithValue( (err as Error).message || "Network error during user update" ); } }); // ---------------- Slice ---------------- const authSlice = createSlice({ name: "auth", initialState, reducers: { setAuthMessage: (state, action: PayloadAction) => { state.authMessage = action.payload; }, clearAuthMessage: state => { state.authMessage = ""; }, clearAddedUser: state => { state.addedUser = null; }, }, extraReducers: builder => { builder // Login .addCase(login.pending, state => { state.status = "loading"; state.authMessage = "Attempting login..."; }) .addCase(login.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = true; state.authMessage = action.payload.message; state.user = action.payload.user; state.tokenInfo = action.payload.tokenInfo; state.mustChangePassword = action.payload.mustChangePassword; }) .addCase(login.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.error = action.payload as string; state.authMessage = action.payload as string; }) // Logout .addCase(logout.fulfilled, (state, action) => { return { ...initialState, status: "succeeded", authMessage: action.payload.message, }; }) .addCase(logout.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; state.authMessage = action.payload as string; }) // Auto Logout .addCase(autoLogout.fulfilled, (state, action) => { return { ...initialState, status: "succeeded", isLoggedIn: false, authMessage: action.payload.message, }; }) .addCase(autoLogout.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; }) // Change Password .addCase(changePassword.pending, state => { state.status = "loading"; state.authMessage = "Changing password..."; }) .addCase(changePassword.fulfilled, (state, action) => { state.status = "succeeded"; state.authMessage = action.payload.message; state.mustChangePassword = action.payload.success; }) .addCase(changePassword.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; state.authMessage = action.payload as string; }) // Validate Auth .addCase(validateAuth.pending, state => { state.status = "loading"; }) .addCase(validateAuth.fulfilled, (state, action) => { state.status = "succeeded"; state.isLoggedIn = true; state.tokenInfo = action.payload.tokenInfo; }) .addCase(validateAuth.rejected, (state, action) => { state.status = "failed"; state.isLoggedIn = false; state.tokenInfo = null; state.error = action.payload as string; }) // Add User .addCase(addUser.pending, state => { state.status = "loading"; state.authMessage = "Creating user..."; state.addedUser = null; }) .addCase(addUser.fulfilled, (state, action) => { state.status = "succeeded"; state.authMessage = action.payload.message; state.addedUser = action.payload.user; }) .addCase(addUser.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; state.authMessage = action.payload as string; state.addedUser = null; }) // Update User Details .addCase(updateUserDetails.pending, state => { state.status = "loading"; state.authMessage = "Updating user details..."; }) .addCase(updateUserDetails.fulfilled, (state, action) => { state.status = "succeeded"; state.user = action.payload.user; state.authMessage = action.payload.message; }) .addCase(updateUserDetails.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; state.authMessage = action.payload as string; }); }, }); export const { setAuthMessage, clearAuthMessage, clearAddedUser } = authSlice.actions; export default authSlice.reducer;