account and protected router

This commit is contained in:
Виталий Лавшонок
2025-11-05 00:08:51 +03:00
parent 994954c817
commit aeab03d35c
14 changed files with 173 additions and 85 deletions

View File

View File

@@ -1,36 +1,63 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from '../../axios';
// 🔹 Функция для декодирования JWT
// 🔹 Декодирование 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; // <-- добавили email
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: null,
refreshToken: null,
jwt: jwtFromStorage || null,
refreshToken: refreshTokenFromStorage || null,
username: null,
email: null, // <-- добавили email
email: null,
id: null,
status: 'idle',
error: null,
};
// 🔹 AsyncThunk: Регистрация
// Если токен есть, подставляем в 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 (
@@ -47,7 +74,7 @@ export const registerUser = createAsyncThunk(
email,
password,
});
return response.data; // { jwt, refreshToken }
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Registration failed',
@@ -56,7 +83,6 @@ export const registerUser = createAsyncThunk(
},
);
// 🔹 AsyncThunk: Логин
export const loginUser = createAsyncThunk(
'auth/login',
async (
@@ -68,7 +94,7 @@ export const loginUser = createAsyncThunk(
username,
password,
});
return response.data; // { jwt, refreshToken }
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Login failed',
@@ -77,7 +103,6 @@ export const loginUser = createAsyncThunk(
},
);
// 🔹 AsyncThunk: Обновление токена
export const refreshToken = createAsyncThunk(
'auth/refresh',
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
@@ -85,7 +110,7 @@ export const refreshToken = createAsyncThunk(
const response = await axios.post('/authentication/refresh', {
refreshToken,
});
return response.data; // { jwt, refreshToken }
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Refresh token failed',
@@ -94,13 +119,12 @@ export const refreshToken = createAsyncThunk(
},
);
// 🔹 AsyncThunk: Получение информации о пользователе
export const fetchWhoAmI = createAsyncThunk(
'auth/whoami',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get('/authentication/whoami');
return response.data; // { username }
return response.data;
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Failed to fetch user info',
@@ -109,22 +133,6 @@ export const fetchWhoAmI = createAsyncThunk(
},
);
// 🔹 AsyncThunk: Загрузка токенов из localStorage
export const loadTokensFromLocalStorage = createAsyncThunk(
'auth/loadTokens',
async () => {
const jwt = localStorage.getItem('jwt');
const refreshToken = localStorage.getItem('refreshToken');
if (jwt && refreshToken) {
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
return { jwt, refreshToken };
} else {
return { jwt: null, refreshToken: null };
}
},
);
// 🔹 Slice
const authSlice = createSlice({
name: 'auth',
@@ -134,7 +142,7 @@ const authSlice = createSlice({
state.jwt = null;
state.refreshToken = null;
state.username = null;
state.email = null; // <-- очистка email
state.email = null;
state.id = null;
state.status = 'idle';
state.error = null;
@@ -144,7 +152,7 @@ const authSlice = createSlice({
},
},
extraReducers: (builder) => {
// Регистрация
// ----------------- Register -----------------
builder.addCase(registerUser.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -154,7 +162,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
// 🔸 Декодируем JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -180,7 +187,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
// Логин
// ----------------- Login -----------------
builder.addCase(loginUser.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -190,7 +197,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
// 🔸 Декодируем JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -216,7 +222,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
// Обновление токена
// ----------------- Refresh -----------------
builder.addCase(refreshToken.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -226,7 +232,6 @@ const authSlice = createSlice({
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
// 🔸 Декодируем JWT
const decoded = decodeJwt(action.payload.jwt);
state.username =
decoded[
@@ -252,7 +257,7 @@ const authSlice = createSlice({
state.error = action.payload as string;
});
// Получение информации о пользователе
// ----------------- WhoAmI -----------------
builder.addCase(fetchWhoAmI.pending, (state) => {
state.status = 'loading';
state.error = null;
@@ -265,35 +270,6 @@ const authSlice = createSlice({
state.status = 'failed';
state.error = action.payload as string;
});
// Загрузка токенов из localStorage
builder.addCase(
loadTokensFromLocalStorage.fulfilled,
(state, action) => {
state.jwt = action.payload.jwt;
state.refreshToken = action.payload.refreshToken;
if (action.payload.jwt) {
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}`;
}
},
);
},
});