Added Logout

This commit is contained in:
Mitchell Magro 2025-07-23 07:37:39 +02:00
parent e2fc2b73c9
commit 7851a6fa24
11 changed files with 314 additions and 79 deletions

View File

@ -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 }
);
}
}

View File

@ -3,7 +3,7 @@ import "./Modal.scss";
interface ModalProps {
open: boolean;
onClose: () => void;
onClose?: () => void;
children: React.ReactNode;
className?: string;
overlayClassName?: string;

View File

@ -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={{

View File

@ -1,4 +1,4 @@
interface ISearchLabel {
export interface ISearchLabel {
label: string;
field: string;
type: string;
@ -9,4 +9,4 @@ export interface IDataTable<TRow, TColumn> {
tableRows: TRow[];
tableColumns: TColumn[];
tableSearchLabels: ISearchLabel[];
}
}

View File

@ -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 | HTMLElement>(null);
@ -24,6 +31,38 @@ export default function AccountMenu() {
setAnchorEl(null);
};
const dispatch = useDispatch<AppDispatch>();
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 (
<>
<IconButton onClick={handleClick} color="inherit">
@ -56,7 +95,19 @@ export default function AccountMenu() {
<ListItemIcon>
<LogoutIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">Sign out</Typography>
<Button
variant="contained"
color="secondary"
onClick={handleLogout}
disabled={isLoggingOut}
startIcon={
isLoggingOut ? (
<CircularProgress size={20} color="inherit" />
) : null
}
>
{isLoggingOut ? "Logging Out..." : "Logout"}
</Button>
</MenuItem>
</Menu>
</>

View File

@ -17,7 +17,7 @@ export default function RootLayout({
<html lang="en">
<body>
<ReduxProvider>
<ThemeRegistry>{children}</ThemeRegistry>
<ThemeRegistry>{children}</ThemeRegistry>
</ReduxProvider>
</body>
</html>

View File

@ -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<AppDispatch>(); // 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 (
<div className="page-container">
<div className="page-container__content">
<h1 className="page-container__title">Payment Backoffice</h1>
<p className="page-container__message--logged-in">
You are logged in. Redirecting to dashboard...
</p>
</div>
</div>
);
}
return (
<div className="page-container">
@ -84,18 +72,11 @@ export default function LoginPage() {
</p>
</div>
{/* Always show the modal on the login page */}
<Modal
open={true} // Always open on the login page
onClose={() => {
/* No direct close for login modal, user must log in */
}}
title="Login to Backoffice"
>
<Modal open={true} title="Login to Payment Cashier">
<LoginModal
onLogin={handleLogin} // Pass the API call function
onLogin={handleLogin}
authMessage={authMessage}
clearAuthMessage={clearAuthMessage}
clearAuthMessage={handleClearAuthMessage}
/>
</Modal>
</div>

View File

@ -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 <Provider store={store}>{children}</Provider>;
}

View File

@ -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<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;
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;

View File

@ -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;

View File

@ -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,
},
});