Add s s 8854yh440
This commit is contained in:
parent
5d59830e52
commit
62da722945
@ -1,110 +1,63 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAppSelector } from "@/redux/store"
|
import { useEffect } from 'react';
|
||||||
import { isLoggedIn, mountCheckIfValid, logOut } from "@/redux/features/auth-slice";
|
import { useRouter } from 'next/navigation';
|
||||||
import { useDispatch } from "react-redux"
|
import { useAppDispatch, useAppSelector } from '@/redux/store';
|
||||||
import { AppDispatch } from '@/redux/store';
|
import { checkAuth, selectAuth } from '@/redux/features/auth-slice';
|
||||||
import { useEffect, useCallback } from "react";
|
import FullScreenLoader from '@/components/common/fullScreenLoader';
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import { ReactNode } from 'react'
|
|
||||||
import FullScreenLoader from "@/components/common/fullScreenLoader";
|
|
||||||
import { load } from '@/redux/features/settings-slice'
|
|
||||||
import ErrorBoundary from '@/components/common/ErrorBoundary';
|
import ErrorBoundary from '@/components/common/ErrorBoundary';
|
||||||
import axios from 'axios'; // Added axios import
|
import { AppDispatch } from '@/redux/store';
|
||||||
|
|
||||||
// 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: React.ReactNode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useAppDispatch();
|
||||||
|
const { isValid, checkIfValidMounted } = useAppSelector(selectAuth);
|
||||||
|
const notAuthRedirectPage = '/login';
|
||||||
|
|
||||||
// Load states
|
// Check authentication status on mount and periodically
|
||||||
const isValid = useAppSelector((state) => state.authReducer.value.isValid);
|
useEffect(() => {
|
||||||
const checkIfValidMounted = useAppSelector((state) => state.authReducer.value.checkIfValidMounted);
|
const checkAuthentication = async () => {
|
||||||
const notAuthRedirectPage = useAppSelector((state) => state.settingsReducer.value.notAuthRedirectPage);
|
try {
|
||||||
const isLoadingSettings = useAppSelector((state) => state.settingsReducer.value.isLoadingSettings);
|
await (dispatch as AppDispatch)(checkAuth());
|
||||||
const loadedFirstTime = useAppSelector((state) => state.settingsReducer.value.loadedFirstTime);
|
} catch (error) {
|
||||||
|
console.error('Authentication check failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle not logged in state
|
// Initial check
|
||||||
const handleNotLoggedIn = useCallback(() => {
|
checkAuthentication();
|
||||||
// Clear any invalid token from cookies
|
|
||||||
|
// Set up periodic check (every 5 minutes)
|
||||||
|
const intervalId = setInterval(checkAuthentication, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// Cleanup interval on component unmount
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// Handle redirection based on auth status
|
||||||
|
useEffect(() => {
|
||||||
|
if (checkIfValidMounted && !isValid) {
|
||||||
|
// Clear any existing token
|
||||||
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
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
|
// Only redirect if not already on the login page
|
||||||
useEffect(() => {
|
if (window.location.pathname !== notAuthRedirectPage) {
|
||||||
if (checkIfValidMounted) return;
|
window.location.href = notAuthRedirectPage;
|
||||||
|
|
||||||
const checkAuth = async () => {
|
|
||||||
try {
|
|
||||||
await dispatch(mountCheckIfValid());
|
|
||||||
await dispatch(isLoggedIn({ NotLoggedInCallback: handleNotLoggedIn }));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Auth check failed:', error);
|
|
||||||
handleNotLoggedIn();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
checkAuth();
|
|
||||||
}, [checkIfValidMounted, dispatch, handleNotLoggedIn]);
|
|
||||||
|
|
||||||
// Load settings
|
|
||||||
useEffect(() => {
|
|
||||||
if (loadedFirstTime || isLoadingSettings) return;
|
|
||||||
|
|
||||||
const loadSettings = async () => {
|
|
||||||
try {
|
|
||||||
await dispatch(load({ page: 1 }));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load settings:', error);
|
|
||||||
}
|
}
|
||||||
};
|
}, [isValid, checkIfValidMounted]);
|
||||||
|
|
||||||
loadSettings();
|
|
||||||
}, [loadedFirstTime, isLoadingSettings, dispatch]);
|
|
||||||
|
|
||||||
// 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
|
// Show loading state while checking auth
|
||||||
if (isValid === null) {
|
if (!checkIfValidMounted) {
|
||||||
return <FullScreenLoader />;
|
return <FullScreenLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// If not valid, we'll be redirected by the effect above
|
||||||
<ErrorBoundary>
|
if (!isValid) {
|
||||||
{isValid ? children : <FullScreenLoader />}
|
return <FullScreenLoader />;
|
||||||
</ErrorBoundary>
|
}
|
||||||
);
|
|
||||||
|
// If valid, render the protected content
|
||||||
|
return <ErrorBoundary>{children}</ErrorBoundary>;
|
||||||
}
|
}
|
||||||
|
|||||||
56
webapp/src/app/api/auth/verify/route.ts
Normal file
56
webapp/src/app/api/auth/verify/route.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { dbConnect } from '@/lib/dbConnect';
|
||||||
|
import User from '@/models/User';
|
||||||
|
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
webapp/src/lib/api.ts
Normal file
50
webapp/src/lib/api.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
47
webapp/src/lib/auth.ts
Normal file
47
webapp/src/lib/auth.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,57 +6,92 @@
|
|||||||
* * source : https://nextjs.org/docs/app/building-your-application/routing/middleware
|
* * source : https://nextjs.org/docs/app/building-your-application/routing/middleware
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import createIntlMiddleware from 'next-intl/middleware';
|
import { NextResponse } from 'next/server';
|
||||||
import { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
import { NextResponse } from 'next/server'
|
import { verifyToken } from '@/lib/auth';
|
||||||
import validateAuthToken from '@/middleware/validateAuthToken'
|
|
||||||
|
|
||||||
export default async function middleware(request: NextRequest) {
|
// Define protected API routes
|
||||||
|
const protectedApiRoutes = [
|
||||||
|
'/api/user',
|
||||||
|
// Add other protected API routes here
|
||||||
|
];
|
||||||
|
|
||||||
// log the request general informations
|
// Define public routes that don't require authentication
|
||||||
let ip = request.ip ?? request.headers.get('X-Forwarded-For')?.split(':')[3]
|
const publicRoutes = [
|
||||||
console.log("request to : " ,request.nextUrl.pathname , 'from ip :' , ip)
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/forgot-password',
|
||||||
|
'/api/auth',
|
||||||
|
'/api/auth/verify',
|
||||||
|
// Add other public routes here
|
||||||
|
];
|
||||||
|
|
||||||
// handle pages
|
export async function middleware(request: NextRequest) {
|
||||||
if(!request.nextUrl.pathname.startsWith('/api'))
|
const { pathname } = request.nextUrl;
|
||||||
{
|
|
||||||
// handle next-intl Internationalization
|
|
||||||
const defaultLocale = request.headers.get('x-default-locale') || 'ar';
|
|
||||||
const handleI18nRouting = createIntlMiddleware({
|
|
||||||
locales: ['ar', 'en'],
|
|
||||||
defaultLocale
|
|
||||||
});
|
|
||||||
const response = handleI18nRouting(request);
|
|
||||||
response.headers.set('x-default-locale', defaultLocale);
|
|
||||||
|
|
||||||
return response;
|
// Skip middleware for public routes
|
||||||
|
if (publicRoutes.some(route => pathname.startsWith(route))) {
|
||||||
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle api routes
|
// Check if the request is for a protected API route
|
||||||
// must be user routes
|
const isProtectedApiRoute = protectedApiRoutes.some(route => pathname.startsWith(route));
|
||||||
|
|
||||||
// protect /api/user routes
|
if (isProtectedApiRoute) {
|
||||||
if(request.nextUrl.pathname.startsWith('/api/user'))
|
try {
|
||||||
{
|
// Verify the token
|
||||||
let authToken : {name:string , value : string} | undefined = request.cookies.get('authToken')
|
const token = request.cookies.get('authToken')?.value;
|
||||||
let authValidation : boolean | undefined = await validateAuthToken(authToken?.value)
|
|
||||||
|
|
||||||
if(!authValidation)
|
if (!token) {
|
||||||
{ // you are not auth you cant access this route
|
return NextResponse.json(
|
||||||
return new NextResponse(
|
{ success: false, message: 'No token provided' },
|
||||||
JSON.stringify({
|
{ status: 401 }
|
||||||
success: false,
|
);
|
||||||
message: "notAllowed",
|
|
||||||
// @ts-ignore
|
|
||||||
} , {status : 405 , headers: { 'content-type': 'application/json'}})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// you can access the api route
|
|
||||||
return NextResponse.next()
|
const isValid = await verifyToken(token);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Invalid or expired token' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is valid, continue with the request
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, message: 'Authentication failed' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// represent the routes that this middleware supposed to handle them
|
// For non-API routes, check if user is authenticated
|
||||||
|
const isAuthenticated = await verifyToken(request.cookies.get('authToken')?.value);
|
||||||
|
|
||||||
|
// If not authenticated and trying to access a protected page, redirect to login
|
||||||
|
if (!isAuthenticated && !pathname.startsWith('/login')) {
|
||||||
|
const loginUrl = new URL('/login', request.url);
|
||||||
|
loginUrl.searchParams.set('from', pathname);
|
||||||
|
return NextResponse.redirect(loginUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure which routes should be processed by the middleware
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
* - public folder
|
||||||
|
*/
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
@ -5,156 +5,119 @@
|
|||||||
* * 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 { RootState } from '@/redux/store';
|
||||||
import Cookies from 'universal-cookie';
|
import api from '@/lib/api';
|
||||||
import { fireAlert } from './alert-slice';
|
import { setLoading } from './ui-slice';
|
||||||
|
|
||||||
const cookies = new Cookies();
|
interface AuthState {
|
||||||
|
|
||||||
type IinitialState = {
|
|
||||||
value: AuthState;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthState = {
|
|
||||||
authToken: string | null;
|
authToken: string | null;
|
||||||
isValid: boolean | null;
|
isValid: boolean | null;
|
||||||
checkIfValidMounted: boolean;
|
checkIfValidMounted: boolean;
|
||||||
|
lastChecked: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState: AuthState = {
|
||||||
value: {
|
authToken: null,
|
||||||
// try to get the token from the browser cookies
|
|
||||||
authToken: cookies.get("authToken") || null,
|
|
||||||
isValid: null,
|
isValid: null,
|
||||||
checkIfValidMounted: false,
|
checkIfValidMounted: false,
|
||||||
}
|
lastChecked: null,
|
||||||
} as IinitialState
|
};
|
||||||
|
|
||||||
const logIn = createAsyncThunk(
|
export const checkAuth = createAsyncThunk(
|
||||||
'auth/logInStatus',
|
'auth/checkAuth',
|
||||||
async (actionPayload: { username: string, password: string, successCallback: any }, thunkAPI) => {
|
async (_, { dispatch, getState }) => {
|
||||||
try {
|
try {
|
||||||
let { data } = await axios.post('/api/auth', actionPayload)
|
const { authReducer } = getState() as { authReducer: AuthState };
|
||||||
if (data.success) {
|
|
||||||
actionPayload.successCallback()
|
|
||||||
// fire the success alert
|
|
||||||
thunkAPI.dispatch(fireAlert({
|
|
||||||
success: true,
|
|
||||||
message: "loggedIn",
|
|
||||||
}))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
thunkAPI.dispatch(fireAlert({
|
|
||||||
success: false,
|
|
||||||
message: data.message
|
|
||||||
}))
|
|
||||||
return { success: false }
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
thunkAPI.dispatch(fireAlert({
|
|
||||||
success: false,
|
|
||||||
message: "unkownError",
|
|
||||||
}))
|
|
||||||
return { success: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const isLoggedIn = createAsyncThunk(
|
// Skip if we've checked recently (within last 5 minutes)
|
||||||
'auth/isLoggedInStatus',
|
if (authReducer.lastChecked && (Date.now() - authReducer.lastChecked < 5 * 60 * 1000)) {
|
||||||
async (actionPayload: { LoggedInCallback?: () => void; NotLoggedInCallback?: () => void }, { dispatch, getState }) => {
|
return { isValid: authReducer.isValid };
|
||||||
try {
|
|
||||||
const state = getState() as RootState;
|
|
||||||
const { authToken } = state.authReducer.value;
|
|
||||||
|
|
||||||
// If no token, immediately trigger not logged in
|
|
||||||
if (!authToken) {
|
|
||||||
if (actionPayload.NotLoggedInCallback) {
|
|
||||||
actionPayload.NotLoggedInCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!authReducer.authToken) {
|
||||||
return { isValid: false };
|
return { isValid: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const response = await api.get('/auth/verify');
|
||||||
const { data } = await axios.get('/api/auth?authToken=' + authToken);
|
return { isValid: response.data.valid };
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error in isLoggedIn:', error);
|
console.error('Auth check failed:', error);
|
||||||
if (actionPayload.NotLoggedInCallback) {
|
|
||||||
actionPayload.NotLoggedInCallback();
|
|
||||||
}
|
|
||||||
return { isValid: false };
|
return { isValid: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
export const auth = createSlice({
|
export const login = createAsyncThunk(
|
||||||
name: "auth",
|
'auth/login',
|
||||||
|
async (credentials: { username: string; password: string }, { dispatch }) => {
|
||||||
|
try {
|
||||||
|
dispatch(setLoading(true));
|
||||||
|
const response = await api.post('/auth/login', credentials);
|
||||||
|
return {
|
||||||
|
authToken: response.data.token,
|
||||||
|
isValid: true
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
dispatch(setLoading(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const logout = createAsyncThunk(
|
||||||
|
'auth/logout',
|
||||||
|
async (_, { dispatch }) => {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/logout');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
// Clear token from cookies
|
||||||
|
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||||
|
return { isValid: false, authToken: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const authSlice = createSlice({
|
||||||
|
name: 'auth',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
logOut: (state) => {
|
setAuthToken: (state, action) => {
|
||||||
// remove the authToken cookie
|
state.authToken = action.payload;
|
||||||
cookies.remove('authToken', { path: '/' });
|
state.isValid = !!action.payload;
|
||||||
// reset the state to there initial value
|
|
||||||
state.value.isValid = null
|
|
||||||
state.value.authToken = null
|
|
||||||
},
|
},
|
||||||
mountCheckIfValid: (state) => {
|
clearAuth: (state) => {
|
||||||
state.value.checkIfValidMounted = true
|
state.authToken = null;
|
||||||
|
state.isValid = false;
|
||||||
|
state.checkIfValidMounted = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// logIn thunk reducer
|
builder
|
||||||
builder.addCase(logIn.fulfilled, (state: IinitialState, action) => {
|
.addCase(checkAuth.fulfilled, (state, action) => {
|
||||||
// set the state
|
state.isValid = action.payload.isValid;
|
||||||
if (action.payload.success) {
|
state.checkIfValidMounted = true;
|
||||||
state.value.authToken = action.payload.authToken;
|
state.lastChecked = Date.now();
|
||||||
state.value.isValid = true
|
|
||||||
|
if (!action.payload.isValid) {
|
||||||
|
state.authToken = null;
|
||||||
|
document.cookie = 'authToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// check if user authToken cookie is a valid one
|
.addCase(login.fulfilled, (state, action) => {
|
||||||
builder.addCase(isLoggedIn.fulfilled, (state: IinitialState, action) => {
|
state.authToken = action.payload.authToken;
|
||||||
// set the state
|
state.isValid = action.payload.isValid;
|
||||||
state.value.isValid = action.payload.isValid;
|
state.lastChecked = Date.now();
|
||||||
state.value.checkIfValidMounted = true
|
|
||||||
})
|
})
|
||||||
|
.addCase(logout.fulfilled, (state) => {
|
||||||
|
state.authToken = null;
|
||||||
|
state.isValid = false;
|
||||||
|
state.lastChecked = null;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// export the functions
|
export const { setAuthToken, clearAuth } = authSlice.actions;
|
||||||
export const { logOut, mountCheckIfValid } = auth.actions;
|
export const selectAuth = (state: RootState) => state.auth;
|
||||||
export { logIn, isLoggedIn };
|
export default authSlice.reducer;
|
||||||
export default auth.reducer;
|
|
||||||
Loading…
Reference in New Issue
Block a user