feature/WNPT-533 #1
4 changed files with 48 additions and 39 deletions
|
|
@ -1,19 +1,13 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAppDispatch, useAppSelector } from '@/utils/hooks';
|
import { useAppSelector } from '@/utils/hooks';
|
||||||
import { hydrateAuth } from '@/features/auth/authSlice';
|
|
||||||
import AdminLayout from '@/components/layout/AdminLayout';
|
import AdminLayout from '@/components/layout/AdminLayout';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(hydrateAuth());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated && location.pathname !== '/login' && location.pathname !== '/verify-otp') {
|
if (!isAuthenticated && location.pathname !== '/login' && location.pathname !== '/verify-otp') {
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,42 @@
|
||||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { AuthState, LoginSuccessPayload } from '@/features/auth/authTypes';
|
import type { AuthState, LoginSuccessPayload } from '@/features/auth/authTypes';
|
||||||
import { STORAGE_KEYS } from '@/utils/storage';
|
import { STORAGE_KEYS } from '@/utils/storage';
|
||||||
import { getAccessToken, storeAuthTokens } from '@/utils/authToken';
|
import { clearAuthTokens, getAccessToken, isAccessTokenExpired, storeAuthTokens } from '@/utils/authToken';
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const clearPersistedAuthState = () => {
|
||||||
user: null,
|
localStorage.removeItem(STORAGE_KEYS.accessToken);
|
||||||
accessToken: null,
|
localStorage.removeItem(STORAGE_KEYS.user);
|
||||||
isAuthenticated: false,
|
localStorage.removeItem(STORAGE_KEYS.authState);
|
||||||
|
clearAuthTokens();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPersistedAuthState = (): AuthState => {
|
||||||
|
const loggedOutState: AuthState = { user: null, accessToken: null, isAuthenticated: false };
|
||||||
|
const storedAuth = localStorage.getItem(STORAGE_KEYS.authState);
|
||||||
|
const storedAccessToken = getAccessToken();
|
||||||
|
|
||||||
|
if (!storedAuth) {
|
||||||
|
return loggedOutState;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(storedAuth) as AuthState;
|
||||||
|
const accessToken = parsed.accessToken || storedAccessToken;
|
||||||
|
|
||||||
|
if (accessToken && parsed.user && !isAccessTokenExpired()) {
|
||||||
|
return { user: parsed.user, accessToken, isAuthenticated: true };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to the logged-out default below
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPersistedAuthState();
|
||||||
|
return loggedOutState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
initialState,
|
initialState: loadPersistedAuthState(),
|
||||||
reducers: {
|
reducers: {
|
||||||
loginSuccess: (state, action: PayloadAction<LoginSuccessPayload>) => {
|
loginSuccess: (state, action: PayloadAction<LoginSuccessPayload>) => {
|
||||||
state.user = action.payload.user;
|
state.user = action.payload.user;
|
||||||
|
|
@ -38,32 +63,9 @@ const authSlice = createSlice({
|
||||||
state.accessToken = null;
|
state.accessToken = null;
|
||||||
state.isAuthenticated = false;
|
state.isAuthenticated = false;
|
||||||
|
|
||||||
localStorage.removeItem(STORAGE_KEYS.accessToken);
|
clearPersistedAuthState();
|
||||||
localStorage.removeItem(STORAGE_KEYS.user);
|
|
||||||
localStorage.removeItem(STORAGE_KEYS.authState);
|
|
||||||
},
|
|
||||||
hydrateAuth: (state) => {
|
|
||||||
const storedAuth = localStorage.getItem(STORAGE_KEYS.authState);
|
|
||||||
const storedAccessToken = getAccessToken();
|
|
||||||
|
|
||||||
if (!storedAuth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(storedAuth) as AuthState;
|
|
||||||
|
|
||||||
if ((parsed.accessToken || storedAccessToken) && parsed.user) {
|
|
||||||
state.user = parsed.user;
|
|
||||||
state.accessToken = parsed.accessToken || storedAccessToken;
|
|
||||||
state.isAuthenticated = true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
state.user = null;
|
|
||||||
state.accessToken = null;
|
|
||||||
state.isAuthenticated = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
hydrateAuth: () => loadPersistedAuthState(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export const authApi = {
|
||||||
storeAuthTokens({
|
storeAuthTokens({
|
||||||
access_token: response.data.access_token,
|
access_token: response.data.access_token,
|
||||||
refresh_token: response.data.refresh_token,
|
refresh_token: response.data.refresh_token,
|
||||||
|
expires_in: response.data.expires_in,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
@ -57,6 +58,7 @@ export const authApi = {
|
||||||
storeAuthTokens({
|
storeAuthTokens({
|
||||||
access_token: response.data.access_token,
|
access_token: response.data.access_token,
|
||||||
refresh_token: response.data.refresh_token,
|
refresh_token: response.data.refresh_token,
|
||||||
|
expires_in: response.data.expires_in,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const ACCESS_TOKEN_KEY = 'access_token';
|
const ACCESS_TOKEN_KEY = 'access_token';
|
||||||
const REFRESH_TOKEN_KEY = 'refresh_token';
|
const REFRESH_TOKEN_KEY = 'refresh_token';
|
||||||
|
const EXPIRES_AT_KEY = 'access_token_expires_at';
|
||||||
|
|
||||||
export const getAccessToken = (): string | null => {
|
export const getAccessToken = (): string | null => {
|
||||||
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
return localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||||
|
|
@ -9,12 +10,22 @@ export const getRefreshToken = (): string | null => {
|
||||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storeAuthTokens = (tokens: { access_token: string; refresh_token: string }) => {
|
export const storeAuthTokens = (tokens: { access_token: string; refresh_token: string; expires_in?: number }) => {
|
||||||
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token);
|
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token);
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token);
|
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token);
|
||||||
|
|
||||||
|
if (tokens.expires_in) {
|
||||||
|
localStorage.setItem(EXPIRES_AT_KEY, String(Date.now() + tokens.expires_in * 1000));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAccessTokenExpired = (): boolean => {
|
||||||
|
const expiresAt = localStorage.getItem(EXPIRES_AT_KEY);
|
||||||
|
return expiresAt !== null && Date.now() >= Number(expiresAt);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearAuthTokens = () => {
|
export const clearAuthTokens = () => {
|
||||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(EXPIRES_AT_KEY);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue