Adding login and logout

This commit is contained in:
Mitchell Magro 2025-07-23 08:45:54 +02:00
parent 5779dd762f
commit adcde37071
12 changed files with 282 additions and 7 deletions

View File

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

View File

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

View File

@ -39,7 +39,7 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
}); });
router.push(`?${updatedParams.toString()}`); router.push(`?${updatedParams.toString()}`);
}, 500), }, 500),
[router], [router]
); );
const handleFieldChange = (field: string, value: string) => { const handleFieldChange = (field: string, value: string) => {
@ -165,7 +165,7 @@ export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
onChange={(newValue) => onChange={(newValue) =>
handleFieldChange( handleFieldChange(
field, field,
newValue?.toISOString() || "", newValue?.toISOString() || ""
) )
} }
slotProps={{ slotProps={{

View File

@ -15,6 +15,7 @@ export default function LoginModal({
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false); 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 // Effect to clear authentication messages when email or password inputs change
useEffect(() => { useEffect(() => {

View File

@ -7,10 +7,17 @@ import {
IconButton, IconButton,
ListItemIcon, ListItemIcon,
Typography, Typography,
Button,
CircularProgress,
} from "@mui/material"; } from "@mui/material";
import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import LogoutIcon from "@mui/icons-material/Logout"; 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() { export default function AccountMenu() {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
@ -24,6 +31,38 @@ export default function AccountMenu() {
setAnchorEl(null); 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 ( return (
<> <>
<IconButton onClick={handleClick} color="inherit"> <IconButton onClick={handleClick} color="inherit">
@ -56,7 +95,19 @@ export default function AccountMenu() {
<ListItemIcon> <ListItemIcon>
<LogoutIcon fontSize="small" /> <LogoutIcon fontSize="small" />
</ListItemIcon> </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> </MenuItem>
</Menu> </Menu>
</> </>

View File

@ -1,5 +1,6 @@
import ThemeRegistry from "@/config/ThemeRegistry"; import ThemeRegistry from "@/config/ThemeRegistry";
import type { Metadata } from "next"; import type { Metadata } from "next";
import ReduxProvider from "./redux/ReduxProvider";
import "../styles/globals.scss"; import "../styles/globals.scss";
export const metadata: Metadata = { export const metadata: Metadata = {
@ -15,7 +16,9 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<ReduxProvider>
<ThemeRegistry>{children}</ThemeRegistry> <ThemeRegistry>{children}</ThemeRegistry>
</ReduxProvider>
</body> </body>
</html> </html>
); );

View File

@ -13,9 +13,9 @@ function LoginPageContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirectPath = searchParams.get("redirect") || "/dashboard"; const redirectPath = searchParams.get("redirect") || "/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
console.log("LoginPageContent rendered", isLoggedIn); // Debugging log to check if the component renders
useEffect(() => { useEffect(() => {
const checkAuthStatus = async () => { const checkAuthStatus = async () => {
// Optionally implement // Optionally implement

View File

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

View File

@ -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<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 { configureStore } from "@reduxjs/toolkit";
import advancedSearchReducer from "./advanedSearch/advancedSearchSlice"; import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
import authReducer from "./auth/authSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
advancedSearch: advancedSearchReducer, advancedSearch: advancedSearchReducer,
authSlice: authReducer,
}, },
}); });