From 7851a6fa24070ab9979cd7597839fa4b2187b2a8 Mon Sep 17 00:00:00 2001 From: Mitchell Magro Date: Wed, 23 Jul 2025 07:37:39 +0200 Subject: [PATCH] Added Logout --- payment-iq/app/api/auth/logout/route.tsx | 24 +++ payment-iq/app/components/Modal/Modal.tsx | 2 +- .../AdvancedSearch/AdvancedSearch.tsx | 6 +- payment-iq/app/features/DataTable/types.ts | 4 +- .../header/accountMenu/AccountMenu.tsx | 53 +++++- payment-iq/app/layout.tsx | 2 +- payment-iq/app/login/page.tsx | 121 ++++++-------- payment-iq/app/redux/ReduxProvider.tsx | 19 ++- payment-iq/app/redux/auth/authSlice.tsx | 152 ++++++++++++++++++ payment-iq/app/redux/auth/selectors.ts | 8 + payment-iq/app/redux/store.ts | 2 + 11 files changed, 314 insertions(+), 79 deletions(-) create mode 100644 payment-iq/app/api/auth/logout/route.tsx create mode 100644 payment-iq/app/redux/auth/authSlice.tsx create mode 100644 payment-iq/app/redux/auth/selectors.ts diff --git a/payment-iq/app/api/auth/logout/route.tsx b/payment-iq/app/api/auth/logout/route.tsx new file mode 100644 index 0000000..d5509d9 --- /dev/null +++ b/payment-iq/app/api/auth/logout/route.tsx @@ -0,0 +1,24 @@ +// 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(request: Request) { + 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". + 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/payment-iq/app/components/Modal/Modal.tsx b/payment-iq/app/components/Modal/Modal.tsx index 7a5a2c6..e0d25ee 100644 --- a/payment-iq/app/components/Modal/Modal.tsx +++ b/payment-iq/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/payment-iq/app/features/AdvancedSearch/AdvancedSearch.tsx b/payment-iq/app/features/AdvancedSearch/AdvancedSearch.tsx index 78948a7..d082fb9 100644 --- a/payment-iq/app/features/AdvancedSearch/AdvancedSearch.tsx +++ b/payment-iq/app/features/AdvancedSearch/AdvancedSearch.tsx @@ -17,7 +17,7 @@ import SearchIcon from "@mui/icons-material/Search"; import RefreshIcon from "@mui/icons-material/Refresh"; import { useSearchParams, useRouter } from "next/navigation"; import { useState, useEffect, useMemo } from "react"; -import { ISearchLabel } from "../pages/transactions/types"; +import { ISearchLabel } from "../DataTable/types"; export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) { const searchParams = useSearchParams(); @@ -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/payment-iq/app/features/DataTable/types.ts b/payment-iq/app/features/DataTable/types.ts index 0327000..fb4c381 100644 --- a/payment-iq/app/features/DataTable/types.ts +++ b/payment-iq/app/features/DataTable/types.ts @@ -1,4 +1,4 @@ -interface ISearchLabel { +export interface ISearchLabel { label: string; field: string; type: string; @@ -9,4 +9,4 @@ export interface IDataTable { tableRows: TRow[]; tableColumns: TColumn[]; tableSearchLabels: ISearchLabel[]; -} \ No newline at end of file +} diff --git a/payment-iq/app/features/dashboard/header/accountMenu/AccountMenu.tsx b/payment-iq/app/features/dashboard/header/accountMenu/AccountMenu.tsx index 7287199..cb30633 100644 --- a/payment-iq/app/features/dashboard/header/accountMenu/AccountMenu.tsx +++ b/payment-iq/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, RootState } 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/payment-iq/app/layout.tsx b/payment-iq/app/layout.tsx index 06637ec..896ccb1 100644 --- a/payment-iq/app/layout.tsx +++ b/payment-iq/app/layout.tsx @@ -17,7 +17,7 @@ export default function RootLayout({ - {children} + {children} diff --git a/payment-iq/app/login/page.tsx b/payment-iq/app/login/page.tsx index c185f15..83b065a 100644 --- a/payment-iq/app/login/page.tsx +++ b/payment-iq/app/login/page.tsx @@ -1,79 +1,67 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; -import LoginModal from "../features/Auth/LoginModal"; // Your LoginModal component - -import "./page.scss"; // Global styles for LoginModal and page +import { useDispatch, useSelector } from "react-redux"; +import LoginModal from "../features/Auth/LoginModal"; import Modal from "../components/Modal/Modal"; +import { AppDispatch } from "../redux/types"; +import { + selectAuthMessage, + selectIsLoggedIn, + selectStatus, +} from "../redux/auth/selectors"; +import { clearAuthMessage, login } from "../redux/auth/authSlice"; +import "./page.scss"; export default function LoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const redirectPath = searchParams.get("redirect") || "/dashboard"; + const isLoggedIn = useSelector(selectIsLoggedIn); + const status = useSelector(selectStatus); + const authMessage = useSelector(selectAuthMessage); - const [authMessage, setAuthMessage] = useState(""); - const [isLoggedIn, setIsLoggedIn] = useState(false); + const dispatch = useDispatch(); // Initialize useDispatch + // Effect to handle redirection after login or if already logged in useEffect(() => { - // Check if already logged in by trying to fetch a protected resource or checking a client-accessible flag - // For HTTP-only cookies, you can't directly read the token here. - // Instead, you'd rely on a server-side check (e.g., in middleware or a Server Component) - // or a simple client-side flag if your backend also sets one (less secure for token itself). - // For this example, we'll assume if they land here, they need to log in. - // A more robust check might involve a quick API call to /api/auth/status - // if the token is in an HTTP-only cookie. - const checkAuthStatus = async () => { - // In a real app, this might be a call to a /api/auth/status endpoint - // that checks the HTTP-only cookie on the server and returns a boolean. - // For now, we'll rely on the middleware to redirect if unauthenticated. - // If the user somehow lands on /login with a valid cookie, the middleware - // should have redirected them already. - }; - checkAuthStatus(); - }, []); - - const handleLogin = async (email: string, password: string) => { - setAuthMessage("Attempting login..."); - try { - const response = await fetch("/api/auth/login", { - // <--- CALLING YOUR INTERNAL ROUTE HANDLER - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - - if (response.ok) { - // Check if the response status is 2xx - // Backend has successfully set the HTTP-only cookie - setAuthMessage("Login successful!"); - setIsLoggedIn(true); - // Redirect to the intended path after successful login - router.replace(redirectPath); - } else { - // Handle login errors (e.g., invalid credentials) - setAuthMessage(data.message || "Login failed. Please try again."); - setIsLoggedIn(false); - } - } catch (error) { - console.error("Login failed:", error); - setAuthMessage("An error occurred during login. Please try again later."); - setIsLoggedIn(false); + if (isLoggedIn && status === "succeeded") { + router.replace(redirectPath); // Redirect to intended path on successful login + } + }, [isLoggedIn, status, router, redirectPath]); + + // Function to handle login attempt, now dispatches Redux thunk + const handleLogin = async (email: string, password: string) => { + // Dispatch the login async thunk + const resultAction = await dispatch(login({ email, password })); + + // Check if login was successful based on the thunk's result + if (login.fulfilled.match(resultAction)) { + return true; // Login successful + } else { + return false; // Login failed } - return isLoggedIn; // Return the current login status }; - const clearAuthMessage = () => setAuthMessage(""); + // Function to clear authentication message, now dispatches Redux action + const handleClearAuthMessage = () => { + dispatch(clearAuthMessage()); + }; - // If user is already logged in (e.g., redirected by middleware to dashboard), - // this page shouldn't be visible. The middleware should handle the primary redirect. - // This `isLoggedIn` state here is more for internal page logic if the user somehow - // bypasses middleware or lands on /login with a valid session. - // For a robust setup, the middleware is key. + // If user is already logged in, show a redirecting message + if (isLoggedIn) { + return ( +
+
+

Payment Backoffice

+

+ You are logged in. Redirecting to dashboard... +

+
+
+ ); + } return (
@@ -84,18 +72,11 @@ export default function LoginPage() {

- {/* Always show the modal on the login page */} - { - /* No direct close for login modal, user must log in */ - }} - title="Login to Backoffice" - > + diff --git a/payment-iq/app/redux/ReduxProvider.tsx b/payment-iq/app/redux/ReduxProvider.tsx index ef50bf2..f6dd9c9 100644 --- a/payment-iq/app/redux/ReduxProvider.tsx +++ b/payment-iq/app/redux/ReduxProvider.tsx @@ -1,7 +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 -export default function ReduxProvider({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/payment-iq/app/redux/auth/authSlice.tsx b/payment-iq/app/redux/auth/authSlice.tsx new file mode 100644 index 0000000..f040f2b --- /dev/null +++ b/payment-iq/app/redux/auth/authSlice.tsx @@ -0,0 +1,152 @@ +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: any) { + // Handle network errors or other unexpected issues + return rejectWithValue(error.message || "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: any) { + // Handle network errors + return rejectWithValue(error.message || "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/payment-iq/app/redux/auth/selectors.ts b/payment-iq/app/redux/auth/selectors.ts new file mode 100644 index 0000000..a5bb736 --- /dev/null +++ b/payment-iq/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/payment-iq/app/redux/store.ts b/payment-iq/app/redux/store.ts index 9609be3..8906f07 100644 --- a/payment-iq/app/redux/store.ts +++ b/payment-iq/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, }, });