refactor the logic of refreshing access_token (#10068)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { refreshAccessTokenOrRelogin } from './refresh-token'
|
||||
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
@@ -356,39 +357,8 @@ const baseFetch = <T>(
|
||||
if (!/^(2|3)\d{2}$/.test(String(res.status))) {
|
||||
const bodyJson = res.json()
|
||||
switch (res.status) {
|
||||
case 401: {
|
||||
if (isPublicAPI) {
|
||||
return bodyJson.then((data: ResponseError) => {
|
||||
if (data.code === 'web_sso_auth_required')
|
||||
requiredWebSSOLogin()
|
||||
|
||||
if (data.code === 'unauthorized') {
|
||||
removeAccessToken()
|
||||
globalThis.location.reload()
|
||||
}
|
||||
|
||||
return Promise.reject(data)
|
||||
})
|
||||
}
|
||||
const loginUrl = `${globalThis.location.origin}/signin`
|
||||
bodyJson.then((data: ResponseError) => {
|
||||
if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent)
|
||||
Toast.notify({ type: 'error', message: data.message, duration: 4000 })
|
||||
else if (data.code === 'not_init_validated' && IS_CE_EDITION)
|
||||
globalThis.location.href = `${globalThis.location.origin}/init`
|
||||
else if (data.code === 'not_setup' && IS_CE_EDITION)
|
||||
globalThis.location.href = `${globalThis.location.origin}/install`
|
||||
else if (location.pathname !== '/signin' || !IS_CE_EDITION)
|
||||
globalThis.location.href = loginUrl
|
||||
else if (!silent)
|
||||
Toast.notify({ type: 'error', message: data.message })
|
||||
}).catch(() => {
|
||||
// Handle any other errors
|
||||
globalThis.location.href = loginUrl
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 401:
|
||||
return Promise.reject(resClone)
|
||||
case 403:
|
||||
bodyJson.then((data: ResponseError) => {
|
||||
if (!silent)
|
||||
@@ -484,7 +454,9 @@ export const upload = (options: any, isPublicAPI?: boolean, url?: string, search
|
||||
export const ssePost = (
|
||||
url: string,
|
||||
fetchOptions: FetchOptionType,
|
||||
{
|
||||
otherOptions: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
isPublicAPI = false,
|
||||
onData,
|
||||
onCompleted,
|
||||
@@ -507,8 +479,7 @@ export const ssePost = (
|
||||
onTextReplace,
|
||||
onError,
|
||||
getAbortController,
|
||||
}: IOtherOptions,
|
||||
) => {
|
||||
} = otherOptions
|
||||
const abortController = new AbortController()
|
||||
|
||||
const options = Object.assign({}, baseOptions, {
|
||||
@@ -532,21 +503,29 @@ export const ssePost = (
|
||||
globalThis.fetch(urlWithPrefix, options as RequestInit)
|
||||
.then((res) => {
|
||||
if (!/^(2|3)\d{2}$/.test(String(res.status))) {
|
||||
res.json().then((data: any) => {
|
||||
if (isPublicAPI) {
|
||||
if (data.code === 'web_sso_auth_required')
|
||||
requiredWebSSOLogin()
|
||||
if (res.status === 401) {
|
||||
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
|
||||
ssePost(url, fetchOptions, otherOptions)
|
||||
}).catch(() => {
|
||||
res.json().then((data: any) => {
|
||||
if (isPublicAPI) {
|
||||
if (data.code === 'web_sso_auth_required')
|
||||
requiredWebSSOLogin()
|
||||
|
||||
if (data.code === 'unauthorized') {
|
||||
removeAccessToken()
|
||||
globalThis.location.reload()
|
||||
}
|
||||
if (res.status === 401)
|
||||
return
|
||||
}
|
||||
Toast.notify({ type: 'error', message: data.message || 'Server Error' })
|
||||
})
|
||||
onError?.('Server Error')
|
||||
if (data.code === 'unauthorized') {
|
||||
removeAccessToken()
|
||||
globalThis.location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
res.json().then((data) => {
|
||||
Toast.notify({ type: 'error', message: data.message || 'Server Error' })
|
||||
})
|
||||
onError?.('Server Error')
|
||||
}
|
||||
return
|
||||
}
|
||||
return handleStream(res, (str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
|
||||
@@ -568,7 +547,54 @@ export const ssePost = (
|
||||
|
||||
// base request
|
||||
export const request = <T>(url: string, options = {}, otherOptions?: IOtherOptions) => {
|
||||
return baseFetch<T>(url, options, otherOptions || {})
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const otherOptionsForBaseFetch = otherOptions || {}
|
||||
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => {
|
||||
if (errResp?.status === 401) {
|
||||
return refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
|
||||
baseFetch<T>(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject)
|
||||
}).catch(() => {
|
||||
const {
|
||||
isPublicAPI = false,
|
||||
silent,
|
||||
} = otherOptionsForBaseFetch
|
||||
const bodyJson = errResp.json()
|
||||
if (isPublicAPI) {
|
||||
return bodyJson.then((data: ResponseError) => {
|
||||
if (data.code === 'web_sso_auth_required')
|
||||
requiredWebSSOLogin()
|
||||
|
||||
if (data.code === 'unauthorized') {
|
||||
removeAccessToken()
|
||||
globalThis.location.reload()
|
||||
}
|
||||
|
||||
return Promise.reject(data)
|
||||
})
|
||||
}
|
||||
const loginUrl = `${globalThis.location.origin}/signin`
|
||||
bodyJson.then((data: ResponseError) => {
|
||||
if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent)
|
||||
Toast.notify({ type: 'error', message: data.message, duration: 4000 })
|
||||
else if (data.code === 'not_init_validated' && IS_CE_EDITION)
|
||||
globalThis.location.href = `${globalThis.location.origin}/init`
|
||||
else if (data.code === 'not_setup' && IS_CE_EDITION)
|
||||
globalThis.location.href = `${globalThis.location.origin}/install`
|
||||
else if (location.pathname !== '/signin' || !IS_CE_EDITION)
|
||||
globalThis.location.href = loginUrl
|
||||
else if (!silent)
|
||||
Toast.notify({ type: 'error', message: data.message })
|
||||
}).catch(() => {
|
||||
// Handle any other errors
|
||||
globalThis.location.href = loginUrl
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
reject(errResp)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// request methods
|
||||
|
75
web/service/refresh-token.ts
Normal file
75
web/service/refresh-token.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { apiPrefix } from '@/config'
|
||||
import { fetchWithRetry } from '@/utils'
|
||||
|
||||
let isRefreshing = false
|
||||
function waitUntilTokenRefreshed() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function _check() {
|
||||
const isRefreshingSign = localStorage.getItem('is_refreshing')
|
||||
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) {
|
||||
setTimeout(() => {
|
||||
_check()
|
||||
}, 1000)
|
||||
}
|
||||
else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
_check()
|
||||
})
|
||||
}
|
||||
|
||||
// only one request can send
|
||||
async function getNewAccessToken(): Promise<void> {
|
||||
try {
|
||||
const isRefreshingSign = localStorage.getItem('is_refreshing')
|
||||
if ((isRefreshingSign && isRefreshingSign === '1') || isRefreshing) {
|
||||
await waitUntilTokenRefreshed()
|
||||
}
|
||||
else {
|
||||
globalThis.localStorage.setItem('is_refreshing', '1')
|
||||
isRefreshing = true
|
||||
const refresh_token = globalThis.localStorage.getItem('refresh_token')
|
||||
|
||||
// Do not use baseFetch to refresh tokens.
|
||||
// If a 401 response occurs and baseFetch itself attempts to refresh the token,
|
||||
// it can lead to an infinite loop if the refresh attempt also returns 401.
|
||||
// To avoid this, handle token refresh separately in a dedicated function
|
||||
// that does not call baseFetch and uses a single retry mechanism.
|
||||
const [error, ret] = await fetchWithRetry(globalThis.fetch(`${apiPrefix}/refresh-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;utf-8',
|
||||
},
|
||||
body: JSON.stringify({ refresh_token }),
|
||||
}))
|
||||
if (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
else {
|
||||
if (ret.status === 401)
|
||||
return Promise.reject(ret)
|
||||
|
||||
const { data } = await ret.json()
|
||||
globalThis.localStorage.setItem('console_token', data.access_token)
|
||||
globalThis.localStorage.setItem('refresh_token', data.refresh_token)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
finally {
|
||||
isRefreshing = false
|
||||
globalThis.localStorage.removeItem('is_refreshing')
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshAccessTokenOrRelogin(timeout: number) {
|
||||
return Promise.race([new Promise<void>((resolve, reject) => setTimeout(() => {
|
||||
isRefreshing = false
|
||||
globalThis.localStorage.removeItem('is_refreshing')
|
||||
reject(new Error('request timeout'))
|
||||
}, timeout)), getNewAccessToken()])
|
||||
}
|
Reference in New Issue
Block a user