feat(web): add login page, auth store, API client, and route guard

- API client with axios interceptors: JWT attach + 401 auto-refresh
- Auth store (Zustand): login/logout/loadFromStorage with localStorage
- Login page: gradient background, Ant Design form, error handling
- Home page: dashboard with statistics cards
- App.tsx: PrivateRoute guard, /login route, auth state restoration
- MainLayout: dynamic user display, logout dropdown, menu navigation
- Users API service: CRUD with pagination support
This commit is contained in:
iven
2026-04-11 03:38:29 +08:00
parent a7cdf67d17
commit 4a03a639a6
9 changed files with 421 additions and 27 deletions

View File

@@ -0,0 +1,84 @@
import axios from 'axios';
const client = axios.create({
baseURL: '/api/v1',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor: attach access token
client.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor: auto-refresh on 401
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: unknown) => void;
}> = [];
function processQueue(error: unknown, token: string | null) {
failedQueue.forEach(({ resolve, reject }) => {
if (token) resolve(token);
else reject(error);
});
failedQueue = [];
}
client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return client(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) throw new Error('No refresh token');
const { data } = await axios.post('/api/v1/auth/refresh', {
refresh_token: refreshToken,
});
const newAccessToken = data.data.access_token;
const newRefreshToken = data.data.refresh_token;
localStorage.setItem('access_token', newAccessToken);
localStorage.setItem('refresh_token', newRefreshToken);
processQueue(null, newAccessToken);
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return client(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
window.location.hash = '#/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default client;