From adcde370716ec9c3b7d04f53c5a615c7cefdfd9a Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Wed, 23 Jul 2025 08:45:54 +0200 Subject: [PATCH] Adding login and logout --- app/api/auth/logout/route.tsx | 28 ++++ app/components/Modal/Modal.tsx | 2 +- .../AdvancedSearch/AdvancedSearch.tsx | 4 +- app/features/Auth/LoginModal.tsx | 1 + app/features/DataTable/types.ts | 2 +- .../header/accountMenu/AccountMenu.tsx | 53 +++++- app/layout.tsx | 5 +- app/login/page.tsx | 2 +- app/redux/ReduxProvider.tsx | 24 +++ app/redux/auth/authSlice.tsx | 158 ++++++++++++++++++ app/redux/auth/selectors.ts | 8 + app/redux/store.ts | 2 + 12 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 app/api/auth/logout/route.tsx create mode 100644 app/redux/ReduxProvider.tsx create mode 100644 app/redux/auth/authSlice.tsx create mode 100644 app/redux/auth/selectors.ts diff --git a/app/api/auth/logout/route.tsx b/app/api/auth/logout/route.tsx new file mode 100644 index 0000000..a3a5ede --- /dev/null +++ b/app/api/auth/logout/route.tsx @@ -0,0 +1,28 @@ +// app/api/auth/logout/route.ts +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +// This is your DELETE handler for the logout endpoint +export async function DELETE() { + try { + // Clear the authentication cookie. + // This MUST match the name of the cookie set during login. + // In your login handler, the cookie is named "auth_token". + (await + // Clear the authentication cookie. + // This MUST match the name of the cookie set during login. + // In your login handler, the cookie is named "auth_token". + cookies()).delete("auth_token"); + + return NextResponse.json( + { success: true, message: "Logged out successfully" }, + { status: 200 } + ); + } catch (error) { + console.error("Logout API error:", error); + return NextResponse.json( + { success: false, message: "Internal server error during logout" }, + { status: 500 } + ); + } +} diff --git a/app/components/Modal/Modal.tsx b/app/components/Modal/Modal.tsx index 7a5a2c6..e0d25ee 100644 --- a/app/components/Modal/Modal.tsx +++ b/app/components/Modal/Modal.tsx @@ -3,7 +3,7 @@ import "./Modal.scss"; interface ModalProps { open: boolean; - onClose: () => void; + onClose?: () => void; children: React.ReactNode; className?: string; overlayClassName?: string; diff --git a/app/features/AdvancedSearch/AdvancedSearch.tsx b/app/features/AdvancedSearch/AdvancedSearch.tsx index 8ffe7df..d082fb9 100644 --- a/app/features/AdvancedSearch/AdvancedSearch.tsx +++ b/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -39,7 +39,7 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { }); router.push(`?${updatedParams.toString()}`); }, 500), - [router], + [router] ); const handleFieldChange = (field: string, value: string) => { @@ -165,7 +165,7 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { onChange={(newValue) => handleFieldChange( field, - newValue?.toISOString() || "", + newValue?.toISOString() || "" ) } slotProps={{ diff --git a/app/features/Auth/LoginModal.tsx b/app/features/Auth/LoginModal.tsx index 35cb614..e1ee7c4 100644 --- a/app/features/Auth/LoginModal.tsx +++ b/app/features/Auth/LoginModal.tsx @@ -15,6 +15,7 @@ export default function LoginModal({ const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); + console.log("LoginModal rendered"); // Debugging log to check if the component renders // Effect to clear authentication messages when email or password inputs change useEffect(() => { diff --git a/app/features/DataTable/types.ts b/app/features/DataTable/types.ts index 697de81..fb4c381 100644 --- a/app/features/DataTable/types.ts +++ b/app/features/DataTable/types.ts @@ -9,4 +9,4 @@ export interface IDataTable { tableRows: TRow[]; tableColumns: TColumn[]; tableSearchLabels: ISearchLabel[]; -} \ No newline at end of file +} diff --git a/app/features/dashboard/header/accountMenu/AccountMenu.tsx b/app/features/dashboard/header/accountMenu/AccountMenu.tsx index 7287199..63f0954 100644 --- a/app/features/dashboard/header/accountMenu/AccountMenu.tsx +++ b/app/features/dashboard/header/accountMenu/AccountMenu.tsx @@ -7,10 +7,17 @@ import { IconButton, ListItemIcon, Typography, + Button, + CircularProgress, } from "@mui/material"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import SettingsIcon from "@mui/icons-material/Settings"; import LogoutIcon from "@mui/icons-material/Logout"; +import { useDispatch, useSelector } from "react-redux"; +import { selectIsLoggedIn, selectStatus } from "@/app/redux/auth/selectors"; +import { logout } from "@/app/redux/auth/authSlice"; +import { AppDispatch } from "@/app/redux/types"; +import { useRouter } from "next/navigation"; export default function AccountMenu() { const [anchorEl, setAnchorEl] = React.useState(null); @@ -24,6 +31,38 @@ export default function AccountMenu() { setAnchorEl(null); }; + const dispatch = useDispatch(); + const router = useRouter(); + + // Select relevant state from your auth slice + const isLoggedIn = useSelector(selectIsLoggedIn); + const authStatus = useSelector(selectStatus); + + // Determine if we're currently in the process of logging out + const isLoggingOut = authStatus === "loading"; + + const handleLogout = async () => { + // Dispatch the logout thunk + const resultAction = await dispatch(logout()); + + // Check if logout was successful based on the action result + if (logout.fulfilled.match(resultAction)) { + console.log("Logout successful, redirecting..."); + router.push("/login"); // Redirect to your login page + } else { + // Handle logout failure (e.g., show an error message) + console.error("Logout failed:", resultAction.payload); + // You might want to display a toast or alert here + } + }; + + console.log("[isLoggedin]", isLoggedIn); + + // Only show the logout button if the user is logged in + if (!isLoggedIn) { + return null; + } + return ( <> @@ -56,7 +95,19 @@ export default function AccountMenu() { - Sign out + diff --git a/app/layout.tsx b/app/layout.tsx index 561d14e..896ccb1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import ThemeRegistry from "@/config/ThemeRegistry"; import type { Metadata } from "next"; +import ReduxProvider from "./redux/ReduxProvider"; import "../styles/globals.scss"; export const metadata: Metadata = { @@ -15,7 +16,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/app/login/page.tsx b/app/login/page.tsx index 99fc3b1..eeef10b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -13,9 +13,9 @@ function LoginPageContent() { const searchParams = useSearchParams(); const redirectPath = searchParams.get("redirect") || "/dashboard"; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isLoggedIn, setIsLoggedIn] = useState(false); + console.log("LoginPageContent rendered", isLoggedIn); // Debugging log to check if the component renders useEffect(() => { const checkAuthStatus = async () => { // Optionally implement diff --git a/app/redux/ReduxProvider.tsx b/app/redux/ReduxProvider.tsx new file mode 100644 index 0000000..f6dd9c9 --- /dev/null +++ b/app/redux/ReduxProvider.tsx @@ -0,0 +1,24 @@ +// app/redux/ReduxProvider.tsx +"use client"; + +import React, { useEffect } from "react"; // Import useEffect +import { Provider } from "react-redux"; +import { store } from "./store"; +import { initializeAuth } from "./auth/authSlice"; + +export default function ReduxProvider({ + children, +}: { + children: React.ReactNode; +}) { + // Get the dispatch function directly from the store for initial dispatch + const dispatch = store.dispatch; + + useEffect(() => { + // Dispatch initializeAuth when the ReduxProvider component mounts on the client. + // This ensures your Redux isLoggedIn state is synced with localStorage after a page refresh. + dispatch(initializeAuth()); + }, [dispatch]); // Dependency array ensures it runs only once on mount + + return {children}; +} diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx new file mode 100644 index 0000000..aba58ee --- /dev/null +++ b/app/redux/auth/authSlice.tsx @@ -0,0 +1,158 @@ +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; +} + +const initialState: AuthState = { + isLoggedIn: false, + authMessage: "", + status: "idle", + error: 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 + } + return data.message || "Login successful"; + } catch (error: unknown) { + // Handle network errors or other unexpected issues + if (error instanceof Error) { + return rejectWithValue(error.message || "Network error during login"); + } + return rejectWithValue("Network error during login"); + } + } +); + +// 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 + } + return data.message || "Logged out successfully"; + } catch (error: unknown) { + // Handle network errors + if (error instanceof Error) { + return rejectWithValue(error.message || "Network error during logout"); + } + return rejectWithValue("Network error during logout"); + } + } +); + +// 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; + state.authMessage = action.payload; + }) + .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; + state.authMessage = action.payload; + }) + .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 + }); + }, +}); + +export const { setAuthMessage, clearAuthMessage, initializeAuth } = + authSlice.actions; + +export default authSlice.reducer; diff --git a/app/redux/auth/selectors.ts b/app/redux/auth/selectors.ts new file mode 100644 index 0000000..a5bb736 --- /dev/null +++ b/app/redux/auth/selectors.ts @@ -0,0 +1,8 @@ +import { RootState } from "../types"; + +export const selectIsLoggedIn = (state: RootState) => + state.authSlice.isLoggedIn; +export const selectStatus = (state: RootState) => state.authSlice?.status; +export const selectError = (state: RootState) => state.authSlice?.error; +export const selectAuthMessage = (state: RootState) => + state.authSlice?.authMessage; diff --git a/app/redux/store.ts b/app/redux/store.ts index 9609be3..8906f07 100644 --- a/app/redux/store.ts +++ b/app/redux/store.ts @@ -1,8 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; import advancedSearchReducer from "./advanedSearch/advancedSearchSlice"; +import authReducer from "./auth/authSlice"; export const store = configureStore({ reducer: { advancedSearch: advancedSearchReducer, + authSlice: authReducer, }, });