Browse Source

refactor: replace promise then with async/await

pull/9784/head
Pranav C 2 weeks ago
parent
commit
5fb1a702ba
  1. 155
      packages/nc-gui/composables/useApi/interceptors.ts
  2. 22
      packages/nc-gui/composables/useGlobal/actions.ts
  3. 2
      packages/nc-gui/composables/useGlobal/index.ts
  4. 4
      packages/nc-gui/composables/useGlobal/types.ts
  5. 12
      packages/nocodb/src/db/CustomKnex.ts
  6. 8
      packages/nocodb/src/services/users/users.service.ts

155
packages/nc-gui/composables/useApi/interceptors.ts

@ -1,13 +1,7 @@
import type { Api } from 'nocodb-sdk'
import { useStorage } from '@vueuse/core'
const DbNotFoundMsg = 'Database config not found'
let refreshTokenPromise: Promise<string> | null = null
export function addAxiosInterceptors(api: Api<any>) {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
const state = useGlobal()
const router = useRouter()
const route = router.currentRoute
@ -18,8 +12,9 @@ export function addAxiosInterceptors(api: Api<any>) {
axiosInstance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'
// Add auth header only if signed in and if `xc-short-token` header is not present (for short-lived tokens used for token generation)
if (state.token.value && !config.headers['xc-short-token']) config.headers['xc-auth'] = state.token.value
if (state.token.value && !config.headers['xc-short-token']) {
config.headers['xc-auth'] = state.token.value
}
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles') && state.previewAs?.value) {
config.headers['xc-preview'] = state.previewAs.value
@ -42,27 +37,22 @@ export function addAxiosInterceptors(api: Api<any>) {
return config
})
// Return a successful response back to the calling service
axiosInstance.interceptors.response.use(
(response) => {
return response
},
// Handle Error
(response) => response,
async (error) => {
const isSharedPage =
route.value?.params?.typeOrId === 'base' || route.value?.params?.typeOrId === 'ERD' || route.value.meta.public
// if cancel request then throw error
if (error.code === 'ERR_CANCELED') return Promise.reject(error)
if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/base/0')
if (error.response?.data?.msg === DbNotFoundMsg) {
return router.replace('/base/0')
}
// Return any error which is not due to authentication back to the calling service
if (!error.response || error.response.status !== 401) {
return Promise.reject(error)
}
// Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/token/refresh') {
await state.signOut({
redirectToSignin: !route.value.meta.public,
@ -71,124 +61,29 @@ export function addAxiosInterceptors(api: Api<any>) {
return Promise.reject(error)
}
// if no active refresh token request in the current session then check the local storage
if (!refreshTokenPromise && isTokenRefreshInProgress.value) {
// if token refresh is already in progress, wait for it to finish and then retry the request if token is available
await until(isTokenRefreshInProgress).toMatch((v) => !v, { timeout: 5000 })
isTokenRefreshInProgress.value = false
// check if the user is signed in by checking the token presence and retry the request with the new token
if (state.token.value) {
return new Promise((resolve, reject) => {
const config = error.config
config.headers['xc-auth'] = state.token.value
axiosInstance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
try {
const token = await state.refreshToken({
axiosInstance,
skipLogout: true,
})
const config = error.config
config.headers['xc-auth'] = token
const response = await axiosInstance.request(config)
return response
} catch (refreshTokenError) {
if (refreshTokenError.code === 'ERR_CANCELED') {
return Promise.reject(refreshTokenError)
}
}
let refreshTokenPromiseRes: (token: string) => void
let refreshTokenPromiseRej: (e: Error) => void
// avoid multiple refresh token requests by multiple requests at the same time
// wait for the first request to finish and then retry the failed requests
if (refreshTokenPromise) {
// if previous refresh token request succeeds use the token and retry request
return refreshTokenPromise
.then((token) => {
// New request with new token
return new Promise((resolve, reject) => {
const config = error.config
config.headers['xc-auth'] = token
axiosInstance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
})
.catch(() => {
// ignore since it could have already been handled and redirected to sign in
})
} else {
isTokenRefreshInProgress.value = true
refreshTokenPromise = new Promise<string>((resolve, reject) => {
refreshTokenPromiseRes = resolve
refreshTokenPromiseRej = reject
await state.signOut({
redirectToSignin: !isSharedPage,
skipApiCall: true,
})
// set a catch on the promise to avoid unhandled promise rejection
refreshTokenPromise
.catch(() => {
// ignore
})
.finally(() => {
isTokenRefreshInProgress.value = false
})
return Promise.reject(error)
}
try {
const token = await state.refreshToken()
refreshTokenPromiseRes(token)
} catch (e) {}
// Try request again with new token
return axiosInstance
.post('/auth/token/refresh', null, {
withCredentials: true,
cancelToken: undefined,
})
.then((token) => {
// New request with new token
const config = error.config
config.headers['xc-auth'] = token.data.token
state.signIn(token.data.token, true)
// resolve the refresh token promise and reset
refreshTokenPromiseRes(token.data.token)
refreshTokenPromise = null
return new Promise((resolve, reject) => {
axiosInstance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
})
.catch(async (refreshTokenError) => {
// skip signout call if request cancelled
if (refreshTokenError.code === 'ERR_CANCELED') {
// reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError)
refreshTokenPromise = null
return Promise.reject(refreshTokenError)
}
await state.signOut({
redirectToSignin: !isSharedPage,
skipApiCall: true,
})
// reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError)
refreshTokenPromise = null
return Promise.reject(error)
})
},
)

22
packages/nc-gui/composables/useGlobal/actions.ts

@ -1,9 +1,9 @@
import { getActivePinia } from 'pinia'
import { useStorage } from '@vueuse/core'
import type { Actions, AppInfo, State } from './types'
import type { Actions, AppInfo, Getters, State } from './types'
import type { NcProjectType } from '#imports'
export function useGlobalActions(state: State): Actions {
export function useGlobalActions(state: State, getters: Getters): Actions {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
const isTokenUpdatedTab = useState('isTokenUpdatedTab', () => false)
@ -66,7 +66,7 @@ export function useGlobalActions(state: State): Actions {
/** manually try to refresh token */
const refreshToken = async ({
axiosInstance = nuxtApp.$api.instance,
axiosInstance,
skipSignOut = false,
}: {
axiosInstance?: any
@ -74,7 +74,23 @@ export function useGlobalActions(state: State): Actions {
} = {}) => {
const nuxtApp = useNuxtApp()
const t = nuxtApp.vueApp.i18n.global.t
// if token refresh is already in progress, wait until it is completed or timeout
if (isTokenRefreshInProgress.value) {
await until(isTokenRefreshInProgress).toMatch((v) => !v, { timeout: 10000 })
// if token is already refreshed and valid return the token
if (getters.signedIn.value && state.token.value) {
isTokenRefreshInProgress.value = false
return state.token.value
}
}
isTokenRefreshInProgress.value = true
if (!axiosInstance) {
axiosInstance = nuxtApp.$api?.instance
}
try {
const response = await axiosInstance.post('/auth/token/refresh', null, {
withCredentials: true,

2
packages/nc-gui/composables/useGlobal/index.ts

@ -40,7 +40,7 @@ export const useGlobal = createGlobalState((): UseGlobalReturn => {
const getters = useGlobalGetters(state)
const actions = useGlobalActions(state)
const actions = useGlobalActions(state, getters)
watch(
state.jwtPayload,

4
packages/nc-gui/composables/useGlobal/types.ts

@ -2,8 +2,8 @@ import type { ComputedRef, Ref, ToRefs } from 'vue'
import type { WritableComputedRef } from '@vue/reactivity'
import type { JwtPayload } from 'jwt-decode'
import type { ProjectRoles } from 'nocodb-sdk'
import type { AxiosInstance } from 'axios'
import type { NcProjectType } from '#imports'
export interface AppInfo {
ncSiteUrl: string
authType: 'jwt' | 'none'
@ -92,7 +92,7 @@ export interface SignOutParams {
export interface Actions {
signOut: (signOutParams?: SignOutParams) => Promise<void>
signIn: (token: string, keepProps?: boolean) => void
refreshToken: () => Promise<void>
refreshToken: (params: { axiosInstance?: AxiosInstance; skipLogout?: boolean }) => Promise<void>
loadAppInfo: () => void
setIsMobileMode: (isMobileMode: boolean) => void
navigateToProject: (params: { workspaceId?: string; baseId?: string; type?: NcProjectType; query?: any }) => void

12
packages/nocodb/src/db/CustomKnex.ts

@ -990,7 +990,11 @@ function parseNestedCondition(obj, qb, pKey?, table?, tableAlias?) {
break;
default:
// if object handle recursively
if (typeof val === 'object' && !Array.isArray(val)) {
if (
typeof val === 'object' &&
!(val instanceof Date) &&
!Array.isArray(val)
) {
qb = parseNestedCondition.call(self, val, qb, key, tn, tableAlias);
} else {
// handle based on operator
@ -1247,7 +1251,11 @@ function parseNestedConditionv2(obj, qb, pKey?, table?, tableAlias?) {
break;
default:
// if object handle recursively
if (typeof val === 'object' && !Array.isArray(val)) {
if (
typeof val === 'object' &&
!(val instanceof Date) &&
!Array.isArray(val)
) {
qb = parseNestedCondition.call(self, val, qb, key, tn, tableAlias);
} else {
// handle based on operator

8
packages/nocodb/src/services/users/users.service.ts

@ -599,11 +599,11 @@ export class UsersService {
if (!user['token_version']) {
user['token_version'] = randomTokenString();
}
await User.update(user.id, {
token_version: user['token_version'],
});
await User.update(user.id, {
token_version: user['token_version'],
});
}
await UserRefreshToken.insert({
token: refreshToken,

Loading…
Cancel
Save