Add s s 8854yh499j40440

This commit is contained in:
yznahmad 2025-06-23 00:01:17 +03:00
parent e5f8e7519d
commit 63a785ba5a
6 changed files with 78 additions and 371 deletions

View File

@ -1,110 +1,62 @@
'use client'; 'use client';
import { useAppSelector } from "@/redux/store" import { useAppSelector } from "@/redux/store"
import { isLoggedIn, mountCheckIfValid, logOut } from "@/redux/features/auth-slice"; import { isLoggedIn , mountCheckIfValid } from "@/redux/features/auth-slice";
import { useDispatch } from "react-redux" import { useDispatch } from "react-redux"
import { AppDispatch } from '@/redux/store'; import { AppDispatch } from '@/redux/store';
import { useEffect, useCallback } from "react"; import { useEffect } from "react";
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import FullScreenLoader from "@/components/common/fullScreenLoader"; import FullScreenLoader from "@/components/common/fullScreenLoader";
import { load } from '@/redux/features/settings-slice' import { load } from '@/redux/features/settings-slice'
import ErrorBoundary from '@/components/common/ErrorBoundary';
import axios from 'axios'; // Added axios import
// HOC for auth pages (only accept auth user) // HOC for auth pages ( only accept auth user )
export default function LocaleLayout({ children }: { children: ReactNode }) { export default function LocaleLayout({children} : { children : ReactNode }) {
const router = useRouter();
const router = useRouter()
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
// load states
// Load states const isValid = useAppSelector((state) => state.authReducer.value.isValid)
const isValid = useAppSelector((state) => state.authReducer.value.isValid); const checkIfValidMounted = useAppSelector((state) => state.authReducer.value.checkIfValidMounted)
const checkIfValidMounted = useAppSelector((state) => state.authReducer.value.checkIfValidMounted); const notAuthRedirectPage = useAppSelector((state) => state.settingsReducer.value.notAuthRedirectPage)
const notAuthRedirectPage = useAppSelector((state) => state.settingsReducer.value.notAuthRedirectPage); const isLoadingSettings = useAppSelector((state) => state.settingsReducer.value.isLoadingSettings)
const isLoadingSettings = useAppSelector((state) => state.settingsReducer.value.isLoadingSettings); const loadedFirstTime = useAppSelector((state) => state.settingsReducer.value.loadedFirstTime)
const loadedFirstTime = useAppSelector((state) => state.settingsReducer.value.loadedFirstTime); // Get redux states
// init isLoggedIn
// Handle not logged in state
const handleNotLoggedIn = useCallback(() => {
// Clear any invalid token from cookies
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Clear Redux state
dispatch(logOut());
// Redirect to login page
router.push(notAuthRedirectPage);
}, [notAuthRedirectPage, router, dispatch]);
// Initialize auth check
useEffect(() => { useEffect(() => {
if (checkIfValidMounted) return; if(checkIfValidMounted) return
dispatch(mountCheckIfValid())
const checkAuth = async () => { async function a()
try { {
await dispatch(mountCheckIfValid()); await dispatch(isLoggedIn({NotLoggedInCallback}))
await dispatch(isLoggedIn({ NotLoggedInCallback: handleNotLoggedIn })); }
} catch (error) { a()
console.error('Auth check failed:', error); }, [])
handleNotLoggedIn(); // load settings
}
};
checkAuth();
}, [checkIfValidMounted, dispatch, handleNotLoggedIn]);
// Load settings
useEffect(() => { useEffect(() => {
if (loadedFirstTime || isLoadingSettings) return; if(loadedFirstTime) return
if(isLoadingSettings) return
const loadSettings = async () => { async function a()
try { {
await dispatch(load({ page: 1 })); await dispatch(load({page : 1}))
} catch (error) { }
console.error('Failed to load settings:', error); a()
} }, [])
}; // if wasnt logged in this will fire
function NotLoggedInCallback()
loadSettings(); {
}, [loadedFirstTime, isLoadingSettings, dispatch]); return router.push(notAuthRedirectPage)
// Add global error handler for API requests
useEffect(() => {
const responseInterceptor = (response: any) => response;
const errorInterceptor = (error: any) => {
if (error.response?.status === 401) {
// Handle unauthorized (token expired/invalid)
handleNotLoggedIn();
}
return Promise.reject(error);
};
// Add request interceptor
const requestInterceptor = axios.interceptors.request.use(
config => config,
error => Promise.reject(error)
);
// Add response interceptor
const responseIntercept = axios.interceptors.response.use(
responseInterceptor,
errorInterceptor
);
// Cleanup function
return () => {
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseIntercept);
};
}, [handleNotLoggedIn]);
// Show loading state while checking auth
if (isValid === null) {
return <FullScreenLoader />;
} }
return ( return (
<ErrorBoundary> <>
{isValid ? children : <FullScreenLoader />} {
</ErrorBoundary> isValid ?
<>
{children}
</>
:
<FullScreenLoader />
}
</>
); );
} }

View File

@ -1,56 +0,0 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';
import { dbConnect } from '@/lib/dbConnect';
import User from '@/database/models/userModel';
export async function GET() {
try {
await dbConnect();
// Get the token from cookies
const cookieStore = cookies();
const token = cookieStore.get('authToken')?.value;
if (!token) {
return NextResponse.json(
{ valid: false, message: 'No token provided' },
{ status: 401 }
);
}
try {
// Verify the token
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
// Check if user still exists
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return NextResponse.json(
{ valid: false, message: 'User not found' },
{ status: 401 }
);
}
return NextResponse.json({ valid: true });
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
return NextResponse.json(
{ valid: false, message: 'Token expired' },
{ status: 401 }
);
}
return NextResponse.json(
{ valid: false, message: 'Invalid token' },
{ status: 401 }
);
}
} catch (error) {
console.error('Token verification error:', error);
return NextResponse.json(
{ valid: false, message: 'Server error' },
{ status: 500 }
);
}
}

View File

@ -1,63 +0,0 @@
'use client';
import { Component, ErrorInfo, ReactNode } from 'react';
import { useRouter } from 'next/navigation';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
private handleReset = () => {
// Clear auth token and reload
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
window.location.href = '/';
};
public render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center min-h-screen p-4">
<div className="text-center max-w-md">
<h2 className="text-2xl font-bold mb-4">
{this.state.error?.message === 'Session expired' ? 'Session Expired' : 'Something went wrong'}
</h2>
<p className="mb-6 text-gray-600 dark:text-gray-300">
{this.state.error?.message === 'Session expired'
? 'Your session has expired. Please log in again.'
: 'An unexpected error occurred. Please try again.'}
</p>
<button
onClick={this.handleReset}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
Return to Home
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -1,50 +0,0 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { store } from '@/redux/store';
import { logOut } from '@/redux/features/auth-slice';
const api: AxiosInstance = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
const token = store.getState().authReducer.value.authToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle 401 errors
api.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
const originalRequest = error.config as any;
// If the error is 401 and we haven't tried to refresh yet
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// Clear any existing auth state
store.dispatch(logOut());
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Redirect to login page
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export default api;

View File

@ -1,47 +0,0 @@
import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';
export async function verifyToken(token?: string): Promise<boolean> {
if (!token) return false;
try {
// Verify the token
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
return !!decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
console.log('Token expired');
} else if (error instanceof jwt.JsonWebTokenError) {
console.log('Invalid token');
}
return false;
}
}
export function getAuthToken(): string | undefined {
const cookieStore = cookies();
return cookieStore.get('authToken')?.value;
}
export function clearAuthToken() {
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}
export function setAuthToken(token: string) {
const expires = new Date();
expires.setTime(expires.getTime() + 3 * 60 * 60 * 1000); // 3 hours
document.cookie = `authToken=${token}; Path=/; Expires=${expires.toUTCString()}; HttpOnly; SameSite=Lax`;
}
// Helper to get user ID from token
export function getUserIdFromToken(token?: string): string | null {
if (!token) return null;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
return decoded.userId;
} catch (error) {
return null;
}
}

View File

@ -5,7 +5,7 @@
* * https://redux-toolkit.js.org/api/createAsyncThunk * * https://redux-toolkit.js.org/api/createAsyncThunk
*/ */
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { createAsyncThunk , createSlice } from '@reduxjs/toolkit'
import axios from 'axios' import axios from 'axios'
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import { fireAlert } from './alert-slice'; import { fireAlert } from './alert-slice';
@ -33,10 +33,11 @@ const initialState = {
const logIn = createAsyncThunk( const logIn = createAsyncThunk(
'auth/logInStatus', 'auth/logInStatus',
async (actionPayload: { username: string, password: string, successCallback: any }, thunkAPI) => { async (actionPayload : {username: string , password: string , successCallback : any}, thunkAPI) => {
try { try {
let { data } = await axios.post('/api/auth', actionPayload) let { data } = await axios.post('/api/auth' , actionPayload)
if (data.success) { if(data.success)
{
actionPayload.successCallback() actionPayload.successCallback()
// fire the success alert // fire the success alert
thunkAPI.dispatch(fireAlert({ thunkAPI.dispatch(fireAlert({
@ -45,78 +46,47 @@ const logIn = createAsyncThunk(
})) }))
return data return data
} }
else { else
{
thunkAPI.dispatch(fireAlert({ thunkAPI.dispatch(fireAlert({
success: false, success: false,
message: data.message message: data.message
})) }))
return { success: false } return { success : false }
} }
} catch (err) { }catch(err) {
thunkAPI.dispatch(fireAlert({ thunkAPI.dispatch(fireAlert({
success: false, success: false,
message: "unkownError", message: "unkownError",
})) }))
return { success: false } return { success : false }
} }
} }
) )
const isLoggedIn = createAsyncThunk( const isLoggedIn = createAsyncThunk(
'auth/isLoggedInStatus', 'auth/isLoggedInStatus',
async (actionPayload: { LoggedInCallback?: () => void; NotLoggedInCallback?: () => void }, { dispatch, getState }) => { async (actionPayload : {LoggedInCallback? : any , NotLoggedInCallback? : any}, thunkAPI) => {
try { try {
const state = getState() as RootState; const state : any = thunkAPI.getState()
const { authToken } = state.authReducer.value; let { data } = await axios.get('/api/auth?authToken='+state.authReducer.value.authToken)
if(!data.success && actionPayload.NotLoggedInCallback)
// If no token, immediately trigger not logged in {
if (!authToken) { actionPayload.NotLoggedInCallback()
if (actionPayload.NotLoggedInCallback) {
actionPayload.NotLoggedInCallback();
}
return { isValid: false };
} }
else if(actionPayload.LoggedInCallback && data.success)
try { {
const { data } = await axios.get('/api/auth?authToken=' + authToken); actionPayload.LoggedInCallback()
if (!data.success) {
// If token is invalid or expired, clear it
if (data.message === 'expiredToken' || data.message === 'invalidToken') {
dispatch(logOut());
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}
if (actionPayload.NotLoggedInCallback) {
actionPayload.NotLoggedInCallback();
}
} else if (actionPayload.LoggedInCallback) {
actionPayload.LoggedInCallback();
}
return { isValid: data.success };
} catch (error: any) {
// Handle network errors or server issues
console.error('Token validation error:', error);
// If it's an auth-related error, clear the token
if (error.response?.status === 401 || error.response?.data?.message === 'expiredToken') {
dispatch(logOut());
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}
if (actionPayload.NotLoggedInCallback) {
actionPayload.NotLoggedInCallback();
}
return { isValid: false };
} }
} catch (error) { return {
console.error('Error in isLoggedIn:', error); isValid : data.success
if (actionPayload.NotLoggedInCallback) { }
actionPayload.NotLoggedInCallback(); }catch(e : any)
{
actionPayload.NotLoggedInCallback()
return {
isValid : false
} }
return { isValid: false };
} }
} }
) )
@ -138,15 +108,16 @@ export const auth = createSlice({
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
// logIn thunk reducer // logIn thunk reducer
builder.addCase(logIn.fulfilled, (state: IinitialState, action) => { builder.addCase(logIn.fulfilled, (state : IinitialState , action) => {
// set the state // set the state
if (action.payload.success) { if(action.payload.success)
{
state.value.authToken = action.payload.authToken; state.value.authToken = action.payload.authToken;
state.value.isValid = true state.value.isValid = true
} }
}) })
// check if user authToken cookie is a valid one // check if user authToken cookie is a valid one
builder.addCase(isLoggedIn.fulfilled, (state: IinitialState, action) => { builder.addCase(isLoggedIn.fulfilled, (state : IinitialState , action) => {
// set the state // set the state
state.value.isValid = action.payload.isValid; state.value.isValid = action.payload.isValid;
state.value.checkIfValidMounted = true state.value.checkIfValidMounted = true
@ -155,6 +126,6 @@ export const auth = createSlice({
}) })
// export the functions // export the functions
export const { logOut, mountCheckIfValid } = auth.actions; export const { logOut , mountCheckIfValid } = auth.actions;
export { logIn, isLoggedIn }; export { logIn , isLoggedIn };
export default auth.reducer; export default auth.reducer;