Files
LiquidCode_Frontend/src/redux/slices/auth.ts
Виталий Лавшонок c6303758e1 Add account and articles updater
2025-11-05 11:43:18 +03:00

278 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from '../../axios';
// 🔹 Декодирование JWT
function decodeJwt(token: string) {
const [, payload] = token.split('.');
const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decodeURIComponent(escape(json)));
}
// 🔹 Типы
interface AuthState {
jwt: string | null;
refreshToken: string | null;
username: string | null;
email: string | null;
id: string | null;
status: 'idle' | 'loading' | 'successful' | 'failed';
error: string | null;
}
// 🔹 Инициализация состояния с синхронной загрузкой из localStorage
const jwtFromStorage = localStorage.getItem('jwt');
const refreshTokenFromStorage = localStorage.getItem('refreshToken');
const initialState: AuthState = {
jwt: jwtFromStorage || null,
refreshToken: refreshTokenFromStorage || null,
username: null,
email: null,
id: null,
status: 'idle',
error: null,
};
// Если токен есть, подставляем в axios и декодируем
if (jwtFromStorage) {
axios.defaults.headers.common['Authorization'] = `Bearer ${jwtFromStorage}`;
try {
const decoded = decodeJwt(jwtFromStorage);
initialState.username =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
] || null;
initialState.email =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
] || null;
initialState.id =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
] || null;
} catch {
localStorage.removeItem('jwt');
localStorage.removeItem('refreshToken');
delete axios.defaults.headers.common['Authorization'];
}
}
// 🔹 AsyncThunk-ы (login/register/refresh/whoami) остаются как были
export const registerUser = createAsyncThunk(
'auth/register',
async (
{
username,
email,
password,
}: { username: string; email: string; password: string },
{ rejectWithValue },
) => {
try {
const response = await axios.post('/authentication/register', {
username,
email,
password,
});
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Registration failed',
);
}
},
);
export const loginUser = createAsyncThunk(
'auth/login',
async (
{ username, password }: { username: string; password: string },
{ rejectWithValue },
) => {
try {
const response = await axios.post('/authentication/login', {
username,
password,
});
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Login failed',
);
}
},
);
export const refreshToken = createAsyncThunk(
'auth/refresh',
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
try {
const response = await axios.post('/authentication/refresh', {
refreshToken,
});
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Refresh token failed',
);
}
},
);
export const fetchWhoAmI = createAsyncThunk(
'auth/whoami',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get('/authentication/whoami');
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Failed to fetch user info',
);
}
},
);
// 🔹 Slice
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.jwt = null;
state.refreshToken = null;
state.username = null;
state.email = null;
state.id = null;
state.status = 'idle';
state.error = null;
localStorage.removeItem('jwt');
localStorage.removeItem('refreshToken');
delete axios.defaults.headers.common['Authorization'];
},
},
extraReducers: (builder) => {
// ----------------- Register -----------------
builder.addCase(registerUser.pending, (state) => {
state.status = 'loading';
state.error = null;
});
builder.addCase(registerUser.fulfilled, (state, action) => {
state.status = 'successful';
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
] || null;
state.email =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
] || null;
state.id =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
] || null;
axios.defaults.headers.common[
'Authorization'
] = `Bearer ${action.payload.jwt}`;
localStorage.setItem('jwt', action.payload.jwt);
localStorage.setItem('refreshToken', action.payload.refreshToken);
});
builder.addCase(registerUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
// ----------------- Login -----------------
builder.addCase(loginUser.pending, (state) => {
state.status = 'loading';
state.error = null;
});
builder.addCase(loginUser.fulfilled, (state, action) => {
state.status = 'successful';
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
] || null;
state.email =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
] || null;
state.id =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
] || null;
axios.defaults.headers.common[
'Authorization'
] = `Bearer ${action.payload.jwt}`;
localStorage.setItem('jwt', action.payload.jwt);
localStorage.setItem('refreshToken', action.payload.refreshToken);
});
builder.addCase(loginUser.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
// ----------------- Refresh -----------------
builder.addCase(refreshToken.pending, (state) => {
state.status = 'loading';
state.error = null;
});
builder.addCase(refreshToken.fulfilled, (state, action) => {
state.status = 'successful';
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
] || null;
state.email =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
] || null;
state.id =
decoded[
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
] || null;
axios.defaults.headers.common[
'Authorization'
] = `Bearer ${action.payload.jwt}`;
localStorage.setItem('jwt', action.payload.jwt);
localStorage.setItem('refreshToken', action.payload.refreshToken);
});
builder.addCase(refreshToken.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
// ----------------- WhoAmI -----------------
builder.addCase(fetchWhoAmI.pending, (state) => {
state.status = 'loading';
state.error = null;
});
builder.addCase(fetchWhoAmI.fulfilled, (state, action) => {
state.status = 'successful';
state.username = action.payload.username;
});
builder.addCase(fetchWhoAmI.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
});
},
});
export const { logout } = authSlice.actions;
export const authReducer = authSlice.reducer;