diff --git a/packages/nc-gui/composables/useApi/interceptors.ts b/packages/nc-gui/composables/useApi/interceptors.ts index b9a1a06b38..44c4c8b671 100644 --- a/packages/nc-gui/composables/useApi/interceptors.ts +++ b/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 | null = null - export function addAxiosInterceptors(api: Api) { - 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) { 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) { 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) { 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((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) - }) }, ) diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index b738b6a372..caa8061c77 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/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, diff --git a/packages/nc-gui/composables/useGlobal/index.ts b/packages/nc-gui/composables/useGlobal/index.ts index 16a9d34805..2662b6e64c 100644 --- a/packages/nc-gui/composables/useGlobal/index.ts +++ b/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, diff --git a/packages/nc-gui/composables/useGlobal/types.ts b/packages/nc-gui/composables/useGlobal/types.ts index 19ccb0c655..d82b6a2542 100644 --- a/packages/nc-gui/composables/useGlobal/types.ts +++ b/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 signIn: (token: string, keepProps?: boolean) => void - refreshToken: () => Promise + refreshToken: (params: { axiosInstance?: AxiosInstance; skipLogout?: boolean }) => Promise loadAppInfo: () => void setIsMobileMode: (isMobileMode: boolean) => void navigateToProject: (params: { workspaceId?: string; baseId?: string; type?: NcProjectType; query?: any }) => void diff --git a/packages/nocodb/src/db/CustomKnex.ts b/packages/nocodb/src/db/CustomKnex.ts index c4075fd06e..0a012cf987 100644 --- a/packages/nocodb/src/db/CustomKnex.ts +++ b/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 diff --git a/packages/nocodb/src/services/users/users.service.ts b/packages/nocodb/src/services/users/users.service.ts index 3f002388e2..998d23a94e 100644 --- a/packages/nocodb/src/services/users/users.service.ts +++ b/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,