feat(users): add reset password functionality for users and implement forgot password page
This commit is contained in:
@@ -1,26 +1,48 @@
|
||||
import { HTTPSTATUS } from '@/constants';
|
||||
import { API_PATH_REFRESH_TOKEN } from '@/constants/api';
|
||||
import { ROUTE_LOGIN } from '@/constants/routes';
|
||||
import { getToken, removeToken } from '@/utils/storage';
|
||||
import { history, RequestConfig } from '@umijs/max';
|
||||
import { apiRefreshToken } from '@/services/master/AuthController';
|
||||
import { checkRefreshTokenExpired } from '@/utils/jwt';
|
||||
import {
|
||||
clearAllData,
|
||||
getAccessToken,
|
||||
getRefreshToken,
|
||||
setAccessToken,
|
||||
} from '@/utils/storage';
|
||||
import { history, request, RequestConfig } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
const codeMessage = {
|
||||
200: 'The server successfully returned the requested data。',
|
||||
201: 'New or modified data succeeded。',
|
||||
202: 'A request has been queued in the background (asynchronous task)。',
|
||||
204: 'Data deleted successfully。',
|
||||
400: 'There is an error in the request sent, the server did not perform the operation of creating or modifying data。',
|
||||
401: 'The user does not have permission (token, username, password is wrong) 。',
|
||||
403: 'User is authorized, but access is prohibited。',
|
||||
404: 'The request issued was for a non-existent record, the server did not operate。',
|
||||
406: 'The requested format is not available。',
|
||||
410: 'The requested resource is permanently deleted and will no longer be available。',
|
||||
422: 'When creating an object, a validation error occurred。',
|
||||
500: 'Server error, please check the server。',
|
||||
502: 'Gateway error。',
|
||||
503: 'Service unavailable, server temporarily overloaded or maintained。',
|
||||
504: 'Gateway timeout。',
|
||||
};
|
||||
// Trạng thái dùng chung cho cơ chế refresh token + hàng đợi
|
||||
let refreshingTokenPromise: Promise<string | null> | null = null;
|
||||
|
||||
async function getValidAccessToken(): Promise<string | null> {
|
||||
const refreshToken = getRefreshToken();
|
||||
|
||||
if (!refreshToken || checkRefreshTokenExpired(refreshToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!refreshingTokenPromise) {
|
||||
refreshingTokenPromise = apiRefreshToken({ refresh_token: refreshToken })
|
||||
.then((resp) => {
|
||||
if (resp?.access_token) {
|
||||
setAccessToken(resp.access_token);
|
||||
return resp.access_token;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Refresh token failed:', err);
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
refreshingTokenPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return refreshingTokenPromise;
|
||||
}
|
||||
|
||||
// Runtime configuration
|
||||
export const handleRequestConfig: RequestConfig = {
|
||||
@@ -28,7 +50,9 @@ export const handleRequestConfig: RequestConfig = {
|
||||
timeout: 20000,
|
||||
validateStatus: (status) => {
|
||||
return (
|
||||
(status >= 200 && status < 300) || status === HTTPSTATUS.HTTP_NOTFOUND
|
||||
(status >= 200 && status < 300) ||
|
||||
status === HTTPSTATUS.HTTP_NOTFOUND ||
|
||||
status === HTTPSTATUS.HTTP_UNAUTHORIZED
|
||||
);
|
||||
},
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
@@ -48,24 +72,25 @@ export const handleRequestConfig: RequestConfig = {
|
||||
// Error catching and handling
|
||||
errorHandler: (error: any) => {
|
||||
if (error.response) {
|
||||
const { status, statusText, data } = error.response;
|
||||
const { status } = error.response;
|
||||
|
||||
// Ưu tiên: codeMessage → backend message → statusText
|
||||
const errMsg =
|
||||
codeMessage[status as keyof typeof codeMessage] ||
|
||||
data?.message ||
|
||||
statusText ||
|
||||
'Unknown error';
|
||||
// const errMsg =
|
||||
// codeMessage[status as keyof typeof codeMessage] ||
|
||||
// data?.message ||
|
||||
// statusText ||
|
||||
// 'Unknown error';
|
||||
|
||||
message.error(`❌ ${status}: ${errMsg}`);
|
||||
if (status === 401) {
|
||||
removeToken();
|
||||
history.push(ROUTE_LOGIN);
|
||||
if (status === HTTPSTATUS.HTTP_UNAUTHORIZED) {
|
||||
// 401 đã được xử lý trong responseInterceptors (refresh token + redirect nếu cần)
|
||||
return;
|
||||
} else if (status === HTTPSTATUS.HTTP_SERVERERROR) {
|
||||
// message.error('💥 Internal server error!');
|
||||
} else {
|
||||
message.error(`❌ ${status}: ${error.message || 'Error'}`);
|
||||
}
|
||||
} else if (error.request) {
|
||||
message.error('🚨 No response from server!');
|
||||
} else if (status) {
|
||||
// message.error('💥 Internal server error!');
|
||||
} else {
|
||||
message.error(`⚠️ Request setup error: ${error.message}`);
|
||||
}
|
||||
@@ -79,7 +104,7 @@ export const handleRequestConfig: RequestConfig = {
|
||||
|
||||
// Chỉ cần thêm token, proxy sẽ xử lý việc redirect đến đúng port
|
||||
// URL sẽ bắt đầu với /api và proxy sẽ chuyển đến hostname:81/api
|
||||
const token = getToken();
|
||||
const token = getAccessToken();
|
||||
return {
|
||||
url: url,
|
||||
options: {
|
||||
@@ -96,14 +121,65 @@ export const handleRequestConfig: RequestConfig = {
|
||||
],
|
||||
|
||||
// Unwrap data from backend response
|
||||
// responseInterceptors: [
|
||||
// (response) => {
|
||||
// const res = response.data as ResponseStructure<any>;
|
||||
// if (res && res.success) {
|
||||
// // ✅ Trả ra data luôn thay vì cả object
|
||||
// return res.data;
|
||||
// }
|
||||
// return response.data;
|
||||
// },
|
||||
// ],
|
||||
responseInterceptors: [
|
||||
async (response: any, options: any) => {
|
||||
const isRefreshRequest = response.url?.includes(API_PATH_REFRESH_TOKEN);
|
||||
const alreadyRetried = options?.skipAuthRefresh === true;
|
||||
|
||||
// Xử lý 401: đưa request vào hàng đợi, gọi refresh token, rồi gọi lại
|
||||
if (
|
||||
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
||||
// Không tự refresh cho chính API refresh token để tránh vòng lặp vô hạn
|
||||
!isRefreshRequest &&
|
||||
!alreadyRetried
|
||||
) {
|
||||
const newToken = await getValidAccessToken();
|
||||
|
||||
// Không refresh được => xoá dữ liệu, điều hướng về trang login
|
||||
if (!newToken) {
|
||||
const { pathname } = history.location;
|
||||
clearAllData();
|
||||
if (pathname !== ROUTE_LOGIN) {
|
||||
history.push(`${ROUTE_LOGIN}?redirect=${pathname}`);
|
||||
} else {
|
||||
history.push(ROUTE_LOGIN);
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error('Unauthorized and refresh token is invalid'),
|
||||
);
|
||||
}
|
||||
|
||||
const newOptions = {
|
||||
...options,
|
||||
headers: {
|
||||
...(options.headers || {}),
|
||||
Authorization: `${newToken}`,
|
||||
},
|
||||
skipAuthRefresh: true,
|
||||
};
|
||||
|
||||
// Gọi lại request gốc với accessToken mới
|
||||
return request(response.url, newOptions);
|
||||
}
|
||||
|
||||
if (
|
||||
response.status === HTTPSTATUS.HTTP_UNAUTHORIZED &&
|
||||
(isRefreshRequest || alreadyRetried)
|
||||
) {
|
||||
clearAllData();
|
||||
history.push(ROUTE_LOGIN);
|
||||
return Promise.reject(new Error('Unauthorized'));
|
||||
}
|
||||
|
||||
// console.log('Response from server: ', response);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
config: response.config,
|
||||
data: response.data,
|
||||
};
|
||||
},
|
||||
] as any,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user