|
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; |
|
import authService from '../../services/authService'; |
|
import cacheService from '../../services/cacheService'; |
|
import cookieService from '../../services/cookieService'; |
|
|
|
|
|
const initialState = { |
|
user: null, |
|
isAuthenticated: false, |
|
loading: 'idle', |
|
error: null, |
|
security: { |
|
isLocked: false, |
|
failedAttempts: 0, |
|
securityScore: 1.0, |
|
lastSecurityCheck: null |
|
}, |
|
cache: { |
|
isRemembered: false, |
|
expiresAt: null, |
|
deviceFingerprint: null |
|
} |
|
}; |
|
|
|
|
|
export const checkCachedAuth = createAsyncThunk( |
|
'auth/checkCachedAuth', |
|
async (_, { rejectWithValue }) => { |
|
try { |
|
|
|
const cachedAuth = await cacheService.getAuthCache(); |
|
if (cachedAuth) { |
|
return { |
|
success: true, |
|
user: cachedAuth.user, |
|
token: cachedAuth.token, |
|
rememberMe: cachedAuth.rememberMe, |
|
expiresAt: cachedAuth.expiresAt |
|
}; |
|
} |
|
|
|
|
|
const cookieAuth = await cookieService.getAuthTokens(); |
|
if (cookieAuth) { |
|
|
|
try { |
|
const response = await authService.getCurrentUser(); |
|
if (response.data.success) { |
|
|
|
await cacheService.setAuthCache({ |
|
token: cookieAuth.accessToken, |
|
user: response.data.user |
|
}, cookieAuth.rememberMe); |
|
|
|
return { |
|
success: true, |
|
user: response.data.user, |
|
token: cookieAuth.accessToken, |
|
rememberMe: cookieAuth.rememberMe, |
|
expiresAt: cookieAuth.rememberMe ? |
|
Date.now() + (7 * 24 * 60 * 60 * 1000) : |
|
Date.now() + (60 * 60 * 1000) |
|
}; |
|
} |
|
} catch (error) { |
|
|
|
await cookieService.clearAuthTokens(); |
|
} |
|
} |
|
|
|
return { success: false }; |
|
} catch (error) { |
|
return rejectWithValue('Authentication check failed'); |
|
} |
|
} |
|
); |
|
|
|
export const autoLogin = createAsyncThunk( |
|
'auth/autoLogin', |
|
async (_, { rejectWithValue }) => { |
|
try { |
|
|
|
const cookieAuth = await cookieService.getAuthTokens(); |
|
const token = cookieAuth?.accessToken || localStorage.getItem('token'); |
|
|
|
if (token) { |
|
|
|
const response = await authService.getCurrentUser(); |
|
if (response.data.success) { |
|
|
|
const rememberMe = cookieAuth?.rememberMe || false; |
|
await cacheService.setAuthCache({ |
|
token: token, |
|
user: response.data.user |
|
}, rememberMe); |
|
|
|
|
|
await cookieService.setAuthTokens(token, rememberMe); |
|
|
|
return { |
|
success: true, |
|
user: response.data.user, |
|
token: token, |
|
rememberMe |
|
}; |
|
} |
|
} |
|
return { success: false }; |
|
} catch (error) { |
|
|
|
localStorage.removeItem('token'); |
|
await cookieService.clearAuthTokens(); |
|
return rejectWithValue('Auto login failed'); |
|
} |
|
} |
|
); |
|
|
|
|
|
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) { |
|
|
|
const rememberMe = credentials.rememberMe || false; |
|
await cacheService.setAuthCache({ |
|
token: result.token, |
|
user: result.user |
|
}, rememberMe); |
|
|
|
|
|
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 { |
|
|
|
await cacheService.clearAuthCache(); |
|
|
|
|
|
await cookieService.clearAuthTokens(); |
|
|
|
|
|
const response = await authService.logout(); |
|
return response.data; |
|
} catch (error) { |
|
|
|
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); |
|
} |
|
} |
|
); |
|
|
|
|
|
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) => { |
|
|
|
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; |
|
}) |
|
|
|
|
|
.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; |
|
}) |
|
|
|
|
|
.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'; |
|
}) |
|
|
|
|
|
.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; |
|
|
|
|
|
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; |
|
}) |
|
|
|
|
|
.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; |
|
|
|
|
|
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'); |
|
}) |
|
|
|
|
|
.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; |