diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts
index 6ea53ca..2fba03e 100644
--- a/src/redux/slices/auth.ts
+++ b/src/redux/slices/auth.ts
@@ -1,25 +1,36 @@
import { createSlice, createAsyncThunk, PayloadAction } 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; // <-- добавили email
+ id: string | null;
status: 'idle' | 'loading' | 'successful' | 'failed';
error: string | null;
}
-// Инициализация состояния
+// 🔹 Инициализация состояния
const initialState: AuthState = {
jwt: null,
refreshToken: null,
username: null,
+ email: null, // <-- добавили email
+ id: null,
status: 'idle',
error: null,
};
-// AsyncThunk: Регистрация
+// 🔹 AsyncThunk: Регистрация
export const registerUser = createAsyncThunk(
'auth/register',
async (
@@ -45,7 +56,7 @@ export const registerUser = createAsyncThunk(
},
);
-// AsyncThunk: Логин
+// 🔹 AsyncThunk: Логин
export const loginUser = createAsyncThunk(
'auth/login',
async (
@@ -66,7 +77,7 @@ export const loginUser = createAsyncThunk(
},
);
-// AsyncThunk: Обновление токена
+// 🔹 AsyncThunk: Обновление токена
export const refreshToken = createAsyncThunk(
'auth/refresh',
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
@@ -74,7 +85,7 @@ export const refreshToken = createAsyncThunk(
const response = await axios.post('/authentication/refresh', {
refreshToken,
});
- return response.data; // { username }
+ return response.data; // { jwt, refreshToken }
} catch (err: any) {
return rejectWithValue(
err.response?.data?.message || 'Refresh token failed',
@@ -83,7 +94,7 @@ export const refreshToken = createAsyncThunk(
},
);
-// AsyncThunk: Получение информации о пользователе
+// 🔹 AsyncThunk: Получение информации о пользователе
export const fetchWhoAmI = createAsyncThunk(
'auth/whoami',
async (_, { rejectWithValue }) => {
@@ -98,10 +109,10 @@ export const fetchWhoAmI = createAsyncThunk(
},
);
-// AsyncThunk: Загрузка токенов из localStorage
+// 🔹 AsyncThunk: Загрузка токенов из localStorage
export const loadTokensFromLocalStorage = createAsyncThunk(
'auth/loadTokens',
- async (_, {}) => {
+ async () => {
const jwt = localStorage.getItem('jwt');
const refreshToken = localStorage.getItem('refreshToken');
@@ -114,7 +125,7 @@ export const loadTokensFromLocalStorage = createAsyncThunk(
},
);
-// Slice
+// 🔹 Slice
const authSlice = createSlice({
name: 'auth',
initialState,
@@ -123,6 +134,8 @@ const authSlice = createSlice({
state.jwt = null;
state.refreshToken = null;
state.username = null;
+ state.email = null; // <-- очистка email
+ state.id = null;
state.status = 'idle';
state.error = null;
localStorage.removeItem('jwt');
@@ -136,118 +149,145 @@ const authSlice = createSlice({
state.status = 'loading';
state.error = null;
});
- builder.addCase(
- registerUser.fulfilled,
- (
- state,
- action: PayloadAction<{ jwt: string; refreshToken: string }>,
- ) => {
- state.status = 'successful';
- state.jwt = action.payload.jwt;
- state.refreshToken = action.payload.refreshToken;
- 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: PayloadAction
) => {
- state.status = 'failed';
- state.error = action.payload;
- },
- );
+ builder.addCase(registerUser.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ // 🔸 Декодируем 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}`;
+ 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;
+ });
// Логин
builder.addCase(loginUser.pending, (state) => {
state.status = 'loading';
state.error = null;
});
- builder.addCase(
- loginUser.fulfilled,
- (
- state,
- action: PayloadAction<{ jwt: string; refreshToken: string }>,
- ) => {
- state.status = 'successful';
- state.jwt = action.payload.jwt;
- state.refreshToken = action.payload.refreshToken;
- 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: PayloadAction) => {
- state.status = 'failed';
- state.error = action.payload;
- },
- );
+ builder.addCase(loginUser.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ // 🔸 Декодируем 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}`;
+ 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;
+ });
// Обновление токена
builder.addCase(refreshToken.pending, (state) => {
state.status = 'loading';
state.error = null;
});
- builder.addCase(
- refreshToken.fulfilled,
- (state, action: PayloadAction<{ username: string }>) => {
- state.status = 'successful';
- state.username = action.payload.username;
- },
- );
- builder.addCase(
- refreshToken.rejected,
- (state, action: PayloadAction) => {
- state.status = 'failed';
- state.error = action.payload;
- },
- );
+ builder.addCase(refreshToken.fulfilled, (state, action) => {
+ state.status = 'successful';
+ state.jwt = action.payload.jwt;
+ state.refreshToken = action.payload.refreshToken;
+
+ // 🔸 Декодируем 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}`;
+ 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;
+ });
// Получение информации о пользователе
builder.addCase(fetchWhoAmI.pending, (state) => {
state.status = 'loading';
state.error = null;
});
- builder.addCase(
- fetchWhoAmI.fulfilled,
- (state, action: PayloadAction<{ username: string }>) => {
- state.status = 'successful';
- state.username = action.payload.username;
- },
- );
- builder.addCase(
- fetchWhoAmI.rejected,
- (state, action: PayloadAction) => {
- state.status = 'failed';
- state.error = action.payload;
- },
- );
+ 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;
+ });
// Загрузка токенов из localStorage
builder.addCase(
loadTokensFromLocalStorage.fulfilled,
- (
- state,
- action: PayloadAction<{
- jwt: string | null;
- refreshToken: string | null;
- }>,
- ) => {
+ (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}`;
diff --git a/src/redux/slices/contests.ts b/src/redux/slices/contests.ts
index f19abcd..a5302ec 100644
--- a/src/redux/slices/contests.ts
+++ b/src/redux/slices/contests.ts
@@ -6,9 +6,16 @@ import axios from '../../axios';
// =====================
export interface Mission {
- missionId: number;
+ id: number;
+ authorId: number;
name: string;
- sortOrder: number;
+ difficulty: number;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+ timeLimitMilliseconds: number;
+ memoryLimitBytes: number;
+ statements: null;
}
export interface Member {
diff --git a/src/styles/index.css b/src/styles/index.css
index b873281..69b3072 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -23,6 +23,7 @@
line-height: 1.5;
background-color: var(--color-liquid-background);
color: rgba(255, 255, 255, 0.87);
+ overflow-x: hidden;
}
#root {
diff --git a/src/views/home/contest/Contest.tsx b/src/views/home/contest/Contest.tsx
index fee728f..d184dd4 100644
--- a/src/views/home/contest/Contest.tsx
+++ b/src/views/home/contest/Contest.tsx
@@ -18,7 +18,6 @@ const Contest = () => {
if (contestIdNumber === null) {
return ;
}
-
const dispatch = useAppDispatch();
const contest = useAppSelector((state) => state.contests.selectedContest);
diff --git a/src/views/home/contest/MissionItem.tsx b/src/views/home/contest/MissionItem.tsx
index 1648e4e..ee9579b 100644
--- a/src/views/home/contest/MissionItem.tsx
+++ b/src/views/home/contest/MissionItem.tsx
@@ -1,6 +1,7 @@
import { cn } from '../../../lib/cn';
import { IconError, IconSuccess } from '../../../assets/icons/missions';
import { useNavigate } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
export interface MissionItemProps {
id: number;
@@ -31,6 +32,8 @@ const MissionItem: React.FC = ({
status,
}) => {
const navigate = useNavigate();
+ const location = useLocation();
+ const path = location.pathname + location.search;
return (
= ({
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
)}
onClick={() => {
- navigate(`/mission/${id}`);
+ navigate(`/mission/${id}?back=${path}`);
}}
>
#{id}
diff --git a/src/views/home/contest/Missions.tsx b/src/views/home/contest/Missions.tsx
index e4ef5a7..00d9824 100644
--- a/src/views/home/contest/Missions.tsx
+++ b/src/views/home/contest/Missions.tsx
@@ -28,8 +28,10 @@ const ContestMissions: FC
= ({ contest }) => {
{contest.missions.map((v, i) => (
))}
diff --git a/src/views/mission/statement/Header.tsx b/src/views/mission/statement/Header.tsx
index 51220a5..979957e 100644
--- a/src/views/mission/statement/Header.tsx
+++ b/src/views/mission/statement/Header.tsx
@@ -9,9 +9,10 @@ import { useNavigate } from 'react-router-dom';
interface HeaderProps {
missionId: number;
+ back?: string;
}
-const Header: React.FC = ({ missionId }) => {
+const Header: React.FC = ({ missionId, back }) => {
const navigate = useNavigate();
return (
@@ -29,7 +30,8 @@ const Header: React.FC = ({ missionId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate('/home/missions');
+ if (back) navigate(back);
+ else navigate('/home/missions');
}}
/>
@@ -39,7 +41,10 @@ const Header: React.FC = ({ missionId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate(`/mission/${missionId - 1}`);
+ if (missionId <= 1) return;
+ if (back)
+ navigate(`/mission/${missionId - 1}?back=${back}`);
+ else navigate(`/mission/${missionId - 1}`);
}}
/>
{missionId}
@@ -48,7 +53,9 @@ const Header: React.FC = ({ missionId }) => {
alt="back"
className="h-[24px] w-[24px] cursor-pointer"
onClick={() => {
- navigate(`/mission/${missionId + 1}`);
+ if (back)
+ navigate(`/mission/${missionId + 1}?back=${back}`);
+ else navigate(`/mission/${missionId + 1}`);
}}
/>