Zelyanoth's picture
feat: enhance CORS and auth handling for production
baaf93b
raw
history blame
14.7 kB
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import authService from '../../services/authService';
import cacheService from '../../services/cacheService';
import cookieService from '../../services/cookieService';
// Initial state
const initialState = {
user: null,
isAuthenticated: false,
loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
error: null,
security: {
isLocked: false,
failedAttempts: 0,
securityScore: 1.0,
lastSecurityCheck: null
},
cache: {
isRemembered: false,
expiresAt: null,
deviceFingerprint: null
}
};
// Async thunks for cache operations
export const checkCachedAuth = createAsyncThunk(
'auth/checkCachedAuth',
async (_, { rejectWithValue }) => {
try {
const isDevelopment = import.meta.env.VITE_NODE_ENV === 'development';
if (isDevelopment) {
console.log('πŸ” [Auth] Starting cached authentication check');
}
// First check cache
const cachedAuth = await cacheService.getAuthCache();
if (cachedAuth) {
if (isDevelopment) {
console.log('πŸ—ƒοΈ [Cache] Found cached authentication data');
}
// Validate that the cached token is still valid by checking expiry
if (cachedAuth.expiresAt && Date.now() < cachedAuth.expiresAt) {
return {
success: true,
user: cachedAuth.user,
token: cachedAuth.token,
rememberMe: cachedAuth.rememberMe,
expiresAt: cachedAuth.expiresAt,
deviceFingerprint: cachedAuth.deviceFingerprint
};
} else {
if (isDevelopment) {
console.log('⏰ [Cache] Cached authentication has expired');
}
// Cache expired, clear it
await cacheService.clearAuthCache();
}
}
// If not in cache or expired, check cookies
if (isDevelopment) {
console.log('πŸͺ [Cookie] Checking for authentication cookies');
}
const cookieAuth = await cookieService.getAuthTokens();
if (cookieAuth?.accessToken) {
if (isDevelopment) {
console.log('πŸͺ [Cookie] Found authentication cookies, validating with API');
}
// Validate token and get user data
try {
const response = await authService.getCurrentUser();
if (response.data.success) {
// Store in cache for next time
await cacheService.setAuthCache({
token: cookieAuth.accessToken,
user: response.data.user
}, cookieAuth.rememberMe);
const expiresAt = cookieAuth.rememberMe ?
Date.now() + (7 * 24 * 60 * 60 * 1000) :
Date.now() + (60 * 60 * 1000);
if (isDevelopment) {
console.log('βœ… [Auth] Cookie authentication validated successfully');
}
return {
success: true,
user: response.data.user,
token: cookieAuth.accessToken,
rememberMe: cookieAuth.rememberMe,
expiresAt: expiresAt,
deviceFingerprint: cookieAuth.deviceFingerprint
};
} else {
if (isDevelopment) {
console.log('❌ [Auth] Cookie authentication returned unsuccessful response');
}
}
} catch (error) {
if (isDevelopment) {
console.error('🚨 [API] Cookie validation failed:', error);
}
// Token invalid, clear cookies
await cookieService.clearAuthTokens();
}
} else {
if (isDevelopment) {
console.log('πŸͺ [Cookie] No authentication cookies found');
}
}
if (isDevelopment) {
console.log('πŸ” [Auth] No valid cached or cookie authentication found');
}
return { success: false };
} catch (error) {
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.error('πŸ” [Auth] Cached authentication check failed:', error);
}
return rejectWithValue('Authentication check failed');
}
}
);
export const autoLogin = createAsyncThunk(
'auth/autoLogin',
async (_, { rejectWithValue }) => {
try {
// Enhanced logging for debugging
const isDevelopment = import.meta.env.VITE_NODE_ENV === 'development';
if (isDevelopment) {
console.log('πŸ” [Auth] Starting auto login process');
}
// Try to get token from cookies first, then fallback to localStorage
let token = null;
let rememberMe = false;
try {
const cookieAuth = await cookieService.getAuthTokens();
token = cookieAuth?.accessToken;
rememberMe = cookieAuth?.rememberMe || false;
if (isDevelopment) {
console.log('πŸͺ [Cookie] Got tokens from cookie service:', { token: !!token, rememberMe });
}
} catch (cookieError) {
if (isDevelopment) {
console.warn('πŸͺ [Cookie] Error getting cookie tokens, trying localStorage:', cookieError.message);
}
}
// If no cookie token, try localStorage
if (!token) {
token = localStorage.getItem('token');
if (isDevelopment) {
console.log('πŸ’Ύ [Storage] Got token from localStorage:', !!token);
}
}
if (token) {
try {
// Try to validate token and get user data
if (isDevelopment) {
console.log('πŸ”‘ [Token] Validating token with API');
}
const response = await authService.getCurrentUser();
if (response.data.success) {
// Update cache and cookies
await cacheService.setAuthCache({
token: token,
user: response.data.user
}, rememberMe);
// Ensure cookies are set
await cookieService.setAuthTokens(token, rememberMe);
if (isDevelopment) {
console.log('βœ… [Auth] Auto login successful');
}
return {
success: true,
user: response.data.user,
token: token,
rememberMe
};
} else {
if (isDevelopment) {
console.log('❌ [Auth] API returned unsuccessful response');
}
}
} catch (apiError) {
if (isDevelopment) {
console.error('🚨 [API] Auto login API call failed:', apiError);
}
}
}
if (isDevelopment) {
console.log('πŸ” [Auth] Auto login failed - no valid token found');
}
return { success: false };
} catch (error) {
// Clear tokens on error
localStorage.removeItem('token');
await cookieService.clearAuthTokens();
if (import.meta.env.VITE_NODE_ENV === 'development') {
console.error('πŸ” [Auth] Auto login error:', error);
}
return rejectWithValue('Auto login failed');
}
}
);
// Async thunks
export const registerUser = createAsyncThunk(
'auth/register',
async (userData, { rejectWithValue }) => {
try {
const response = await authService.register(userData);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await authService.login(credentials);
const result = response.data;
if (result.success) {
// Store auth data in cache
const rememberMe = credentials.rememberMe || false;
await cacheService.setAuthCache({
token: result.token,
user: result.user
}, rememberMe);
// Store tokens in secure cookies
await cookieService.setAuthTokens(result.token, rememberMe);
return {
...result,
rememberMe,
expiresAt: rememberMe ? Date.now() + (7 * 24 * 60 * 60 * 1000) : Date.now() + (60 * 60 * 1000)
};
}
return result;
} catch (error) {
return rejectWithValue(error.response?.data || { success: false, message: 'Login failed' });
}
}
);
export const logoutUser = createAsyncThunk(
'auth/logout',
async (_, { rejectWithValue }) => {
try {
// Clear cache first
await cacheService.clearAuthCache();
// Clear cookies
await cookieService.clearAuthTokens();
// Then call logout API
const response = await authService.logout();
return response.data;
} catch (error) {
// Even if API fails, clear cache and cookies
await cacheService.clearAuthCache();
await cookieService.clearAuthTokens();
return { success: true, message: 'Logged out successfully' };
}
}
);
export const getCurrentUser = createAsyncThunk(
'auth/getCurrentUser',
async (_, { rejectWithValue }) => {
try {
const response = await authService.getCurrentUser();
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Auth slice
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
clearError: (state) => {
state.error = null;
state.loading = 'idle';
},
setUser: (state, action) => {
state.user = action.payload.user;
state.isAuthenticated = true;
state.loading = 'succeeded';
state.error = null;
},
clearAuth: (state) => {
state.user = null;
state.isAuthenticated = false;
state.loading = 'idle';
state.error = null;
state.security = {
isLocked: false,
failedAttempts: 0,
securityScore: 1.0,
lastSecurityCheck: null
};
state.cache = {
isRemembered: false,
expiresAt: null,
deviceFingerprint: null
};
},
updateSecurityStatus: (state, action) => {
state.security = { ...state.security, ...action.payload };
},
updateCacheInfo: (state, action) => {
state.cache = { ...state.cache, ...action.payload };
},
setRememberMe: (state, action) => {
state.cache.isRemembered = action.payload;
}
},
extraReducers: (builder) => {
// Check cached authentication
builder
.addCase(checkCachedAuth.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(checkCachedAuth.fulfilled, (state, action) => {
state.loading = 'succeeded';
if (action.payload.success) {
state.user = action.payload.user;
state.isAuthenticated = true;
state.cache.isRemembered = action.payload.rememberMe || false;
state.cache.expiresAt = action.payload.expiresAt;
state.cache.deviceFingerprint = action.payload.deviceFingerprint;
} else {
state.isAuthenticated = false;
}
})
.addCase(checkCachedAuth.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload;
state.isAuthenticated = false;
})
// Auto login
.addCase(autoLogin.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(autoLogin.fulfilled, (state, action) => {
state.loading = 'succeeded';
if (action.payload.success) {
state.user = action.payload.user;
state.isAuthenticated = true;
} else {
state.isAuthenticated = false;
}
})
.addCase(autoLogin.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload;
state.isAuthenticated = false;
})
// Register user (existing)
.addCase(registerUser.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(registerUser.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.user = action.payload.user;
state.isAuthenticated = true;
})
.addCase(registerUser.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload?.message || 'Registration failed';
})
// Login user (enhanced)
.addCase(loginUser.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.user = action.payload.user;
state.isAuthenticated = true;
state.cache.isRemembered = action.payload.rememberMe || false;
state.cache.expiresAt = action.payload.expiresAt;
// Store token securely
localStorage.setItem('token', action.payload.token);
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload?.message || 'Login failed';
state.security.failedAttempts += 1;
})
// Logout user (enhanced)
.addCase(logoutUser.fulfilled, (state) => {
state.user = null;
state.isAuthenticated = false;
state.loading = 'idle';
state.cache.isRemembered = false;
state.cache.expiresAt = null;
state.cache.deviceFingerprint = null;
// Clear all cached data (already done in the thunk)
localStorage.removeItem('token');
})
.addCase(logoutUser.rejected, (state) => {
state.user = null;
state.isAuthenticated = false;
state.loading = 'idle';
state.cache.isRemembered = false;
state.cache.expiresAt = null;
state.cache.deviceFingerprint = null;
localStorage.removeItem('token');
})
// Get current user (existing)
.addCase(getCurrentUser.pending, (state) => {
state.loading = 'pending';
})
.addCase(getCurrentUser.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.user = action.payload.user;
state.isAuthenticated = true;
})
.addCase(getCurrentUser.rejected, (state) => {
state.loading = 'failed';
state.user = null;
state.isAuthenticated = false;
});
}
});
export const {
clearError,
setUser,
clearAuth,
updateSecurityStatus,
updateCacheInfo,
setRememberMe
} = authSlice.actions;
export default authSlice.reducer;