Adding login and logout
This commit is contained in:
parent
5779dd762f
commit
adcde37071
28
app/api/auth/logout/route.tsx
Normal file
28
app/api/auth/logout/route.tsx
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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={{
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>
|
||||||
<ThemeRegistry>{children}</ThemeRegistry>
|
<ReduxProvider>
|
||||||
|
<ThemeRegistry>{children}</ThemeRegistry>
|
||||||
|
</ReduxProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
24
app/redux/ReduxProvider.tsx
Normal file
24
app/redux/ReduxProvider.tsx
Normal 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>;
|
||||||
|
}
|
||||||
158
app/redux/auth/authSlice.tsx
Normal file
158
app/redux/auth/authSlice.tsx
Normal 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;
|
||||||
8
app/redux/auth/selectors.ts
Normal file
8
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 { 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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user