feat(projects): add request refresh token & logout

This commit is contained in:
Soybean 2024-03-24 05:11:30 +08:00
parent 1ed33dc47a
commit 11a6a3bd80
5 changed files with 93 additions and 24 deletions

View File

@ -105,13 +105,13 @@ function createCommonRequest<ResponseData = any>(
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any>(
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance = async function request<T = any, R extends ResponseType = 'json'>(
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);
@ -123,7 +123,7 @@ export function createRequest<ResponseData = any>(
}
return response.data as MappedType<R, T>;
} as RequestInstance;
} as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;
@ -139,13 +139,13 @@ export function createRequest<ResponseData = any>(
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any>(
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance = async function flatRequest<T = any, R extends ResponseType = 'json'>(
const flatRequest: FlatRequestInstance<State> = async function flatRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
try {
@ -163,10 +163,11 @@ export function createFlatRequest<ResponseData = any>(
} catch (error) {
return { data: null, error };
}
} as FlatRequestInstance;
} as FlatRequestInstance<State>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State;
return flatRequest;
}

View File

@ -34,7 +34,7 @@ export interface RequestOption<ResponseData = any> {
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse> | Promise<void>;
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
@ -68,11 +68,16 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
responseType?: R;
};
/** The request instance */
export interface RequestInstance {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
export interface RequestInstanceCommon<T> {
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;
}
/** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<T = any> = {
@ -87,10 +92,8 @@ export type FlatResponseFailData<T = any> = {
export type FlatResponseData<T = any> = FlatResponseSuccessData<T> | FlatResponseFailData<T>;
export interface FlatRequestInstance {
export interface FlatRequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>>>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}

View File

@ -37,13 +37,12 @@ export function fetchRefreshToken(refreshToken: string) {
});
}
export function fetchDebug() {
return request<string>({
url: '/debug-post',
method: 'post',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: {
a: '1'
}
});
/**
* return custom backend error
*
* @param code error code
* @param msg error message
*/
export function fetchCustomBackendError(code: string, msg: string) {
return request({ url: '/auth/error', params: { code, msg } });
}

View File

@ -1,11 +1,19 @@
import type { AxiosResponse } from 'axios';
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { handleRefreshToken } from './shared';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
export const request = createFlatRequest<App.Service.Response>(
interface InstanceState {
/** whether the request is refreshing token */
isRefreshingToken: boolean;
}
export const request = createFlatRequest<App.Service.Response, InstanceState>(
{
baseURL,
headers: {
@ -28,9 +36,36 @@ export const request = createFlatRequest<App.Service.Response>(
// you can change this logic by yourself
return response.data.code === '0000';
},
async onBackendFail(_response) {
async onBackendFail(response, instance) {
// when the backend response code is not "0000", it means the request is fail
// for example: the token is expired, refresh token and retry request
const authStore = useAuthStore();
// when the backend response code is "8888", it means logout the system and redirect to login page
// the following code is an example, you can change it by yourself
const logoutCodes: (string | number)[] = ['8888'];
if (logoutCodes.includes(response.data.code)) {
authStore.resetStore();
return null;
}
// when the backend response code is "9999", it means the token is expired, and refresh token
// the api `refreshToken` can not return the code "9999", otherwise it will be a dead loop, can return `logoutCodes`
const refreshTokenCodes: (string | number)[] = ['9999'];
if (refreshTokenCodes.includes(response.data.code) && !request.state.isRefreshingToken) {
request.state.isRefreshingToken = true;
const refreshConfig = await handleRefreshToken(response.config);
request.state.isRefreshingToken = false;
if (refreshConfig) {
return instance.request(refreshConfig) as Promise<AxiosResponse>;
}
}
return null;
},
transformBackendResponse(response) {
return response.data.data;

View File

@ -0,0 +1,31 @@
import type { AxiosRequestConfig } from 'axios';
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { fetchRefreshToken } from '../api';
/**
* refresh token
*
* @param axiosConfig - request config when the token is expired
*/
export async function handleRefreshToken(axiosConfig: AxiosRequestConfig) {
const { resetStore } = useAuthStore();
const refreshToken = localStg.get('refreshToken') || '';
const { error, data } = await fetchRefreshToken(refreshToken);
if (!error) {
localStg.set('token', data.token);
localStg.set('refreshToken', data.refreshToken);
const config = { ...axiosConfig };
if (config.headers) {
config.headers.Authorization = data.token;
}
return config;
}
resetStore();
return null;
}