Added Logout
This commit is contained in:
parent
e2fc2b73c9
commit
7851a6fa24
24
payment-iq/app/api/auth/logout/route.tsx
Normal file
24
payment-iq/app/api/auth/logout/route.tsx
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import "./Modal.scss";
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
overlayClassName?: string;
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -17,7 +17,7 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ReduxProvider>
|
||||
<ThemeRegistry>{children}</ThemeRegistry>
|
||||
<ThemeRegistry>{children}</ThemeRegistry>
|
||||
</ReduxProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
152
payment-iq/app/redux/auth/authSlice.tsx
Normal file
152
payment-iq/app/redux/auth/authSlice.tsx
Normal 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;
|
||||
8
payment-iq/app/redux/auth/selectors.ts
Normal file
8
payment-iq/app/redux/auth/selectors.ts
Normal 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;
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user