Zelyanoth's picture
fff
25f22bf
raw
history blame
11 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 {
// First check cache
const cachedAuth = await cacheService.getAuthCache();
if (cachedAuth) {
return {
success: true,
user: cachedAuth.user,
token: cachedAuth.token,
rememberMe: cachedAuth.rememberMe,
expiresAt: cachedAuth.expiresAt
};
}
// If not in cache, check cookies
const cookieAuth = await cookieService.getAuthTokens();
if (cookieAuth) {
// 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);
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) {
// Token invalid, clear cookies
await cookieService.clearAuthTokens();
}
}
return { success: false };
} catch (error) {
return rejectWithValue('Authentication check failed');
}
}
);
export const autoLogin = createAsyncThunk(
'auth/autoLogin',
async (_, { rejectWithValue }) => {
try {
// Try to get token from cookies first
const cookieAuth = await cookieService.getAuthTokens();
const token = cookieAuth?.accessToken || localStorage.getItem('token');
if (token) {
// Try to validate token and get user data
const response = await authService.getCurrentUser();
if (response.data.success) {
// Update cache and cookies
const rememberMe = cookieAuth?.rememberMe || false;
await cacheService.setAuthCache({
token: token,
user: response.data.user
}, rememberMe);
// Ensure cookies are set
await cookieService.setAuthTokens(token, rememberMe);
return {
success: true,
user: response.data.user,
token: token,
rememberMe
};
}
}
return { success: false };
} catch (error) {
// Clear tokens on error
localStorage.removeItem('token');
await cookieService.clearAuthTokens();
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;