From c631dee8cbae57e92072cf08b2d5a9c757cb7f02 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:08 +0000 Subject: [PATCH 01/11] refactor: apply refresh token expiry for cookie as well --- packages/nocodb/src/constants/index.ts | 7 +++++++ packages/nocodb/src/models/UserRefreshToken.ts | 9 +-------- packages/nocodb/src/services/users/helpers.ts | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/nocodb/src/constants/index.ts b/packages/nocodb/src/constants/index.ts index 0016f24fd8..d671871dd5 100644 --- a/packages/nocodb/src/constants/index.ts +++ b/packages/nocodb/src/constants/index.ts @@ -6,3 +6,10 @@ export const NC_ATTACHMENT_FIELD_SIZE = +process.env['NC_ATTACHMENT_FIELD_SIZE'] || 20 * 1024 * 1024; // 20 MB export const NC_MAX_ATTACHMENTS_ALLOWED = +process.env['NC_MAX_ATTACHMENTS_ALLOWED'] || 10; +export const NC_REFRESH_TOKEN_EXP_IN_DAYS = + parseInt(process.env.NC_REFRESH_TOKEN_EXP_IN_DAYS, 10) || 90; + +// throw error if user provided invalid value +if (!NC_REFRESH_TOKEN_EXP_IN_DAYS || NC_REFRESH_TOKEN_EXP_IN_DAYS <= 0) { + throw new Error('NC_REFRESH_TOKEN_EXP_IN_DAYS must be a positive number'); +} diff --git a/packages/nocodb/src/models/UserRefreshToken.ts b/packages/nocodb/src/models/UserRefreshToken.ts index d8def4b1e4..7da9d1f954 100644 --- a/packages/nocodb/src/models/UserRefreshToken.ts +++ b/packages/nocodb/src/models/UserRefreshToken.ts @@ -3,14 +3,7 @@ import Noco from '~/Noco'; import { extractProps } from '~/helpers/extractProps'; import { MetaTable, RootScopes } from '~/utils/globals'; import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils'; - -const NC_REFRESH_TOKEN_EXP_IN_DAYS = - parseInt(process.env.NC_REFRESH_TOKEN_EXP_IN_DAYS, 10) || 90; - -// throw error if user provided invalid value -if (NC_REFRESH_TOKEN_EXP_IN_DAYS <= 0) { - throw new Error('NC_REFRESH_TOKEN_EXP_IN_DAYS must be a positive number'); -} +import { NC_REFRESH_TOKEN_EXP_IN_DAYS } from '~/constants'; export default class UserRefreshToken { fk_user_id: string; diff --git a/packages/nocodb/src/services/users/helpers.ts b/packages/nocodb/src/services/users/helpers.ts index 2b2ba15c2a..e4906b0b2e 100644 --- a/packages/nocodb/src/services/users/helpers.ts +++ b/packages/nocodb/src/services/users/helpers.ts @@ -3,6 +3,7 @@ import * as jwt from 'jsonwebtoken'; import type User from '~/models/User'; import type { NcConfig } from '~/interface/config'; import type { Response } from 'express'; +import {NC_REFRESH_TOKEN_EXP_IN_DAYS} from "~/constants"; export function genJwt( user: User & { extra?: Record }, @@ -30,7 +31,7 @@ export function setTokenCookie(res: Response, token): void { // create http only cookie with refresh token that expires in 7 days const cookieOptions = { httpOnly: true, - expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + expires: new Date(Date.now() + NC_REFRESH_TOKEN_EXP_IN_DAYS * 24 * 60 * 60 * 1000), domain: process.env.NC_BASE_HOST_NAME || undefined, }; res.cookie('refresh_token', token, cookieOptions); From bfad4d821a3b3bf6700413ca1871dbbdbfde5383 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:10 +0000 Subject: [PATCH 02/11] refactor: use same pattern for localStorage key name --- packages/nc-gui/utils/ncUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/utils/ncUtils.ts b/packages/nc-gui/utils/ncUtils.ts index c2a581984f..7d52153357 100644 --- a/packages/nc-gui/utils/ncUtils.ts +++ b/packages/nc-gui/utils/ncUtils.ts @@ -1,2 +1,2 @@ export const isEeUI = false -export const TOKEN_REFRESH_PROGRESS_KEY = 'nc_token_refresh_progress' +export const TOKEN_REFRESH_PROGRESS_KEY = 'nc-token-refresh-progress' From 4d8148953071cc1ca9fb01153c3bb0b7f482de5b Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:14 +0000 Subject: [PATCH 03/11] refactor: token generation with token (WIP) --- .../nc-gui/composables/useApi/interceptors.ts | 5 ++ .../nc-gui/composables/useGlobal/actions.ts | 52 +++++++++++-------- .../nc-gui/composables/useGlobal/types.ts | 4 +- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/packages/nc-gui/composables/useApi/interceptors.ts b/packages/nc-gui/composables/useApi/interceptors.ts index d194a5a590..b9a1a06b38 100644 --- a/packages/nc-gui/composables/useApi/interceptors.ts +++ b/packages/nc-gui/composables/useApi/interceptors.ts @@ -137,6 +137,11 @@ export function addAxiosInterceptors(api: Api) { }) } + try { + const token = await state.refreshToken() + refreshTokenPromiseRes(token) + } catch (e) {} + // Try request again with new token return axiosInstance .post('/auth/token/refresh', null, { diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index 5187b77d2f..b738b6a372 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -1,8 +1,10 @@ import { getActivePinia } from 'pinia' +import { useStorage } from '@vueuse/core' import type { Actions, AppInfo, State } from './types' import type { NcProjectType } from '#imports' export function useGlobalActions(state: State): Actions { + const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false) const isTokenUpdatedTab = useState('isTokenUpdatedTab', () => false) const setIsMobileMode = (isMobileMode: boolean) => { @@ -45,7 +47,7 @@ export function useGlobalActions(state: State): Actions { /** Sign in by setting the token in localStorage * keepProps - is for keeping any existing role info if user id is same as previous user * */ - const signIn: Actions['signIn'] = async (newToken, keepProps = false) => { + const signIn: Actions['signIn'] = (newToken, keepProps = false) => { isTokenUpdatedTab.value = true state.token.value = newToken @@ -63,30 +65,36 @@ export function useGlobalActions(state: State): Actions { } /** manually try to refresh token */ - const refreshToken = async () => { + const refreshToken = async ({ + axiosInstance = nuxtApp.$api.instance, + skipSignOut = false, + }: { + axiosInstance?: any + skipSignOut?: boolean + } = {}) => { const nuxtApp = useNuxtApp() const t = nuxtApp.vueApp.i18n.global.t - - return new Promise((resolve) => { - nuxtApp.$api.instance - .post('/auth/token/refresh', null, { - withCredentials: true, - }) - .then((response) => { - if (response.data?.token) { - signIn(response.data.token, true) - } - }) - .catch(async () => { - if (state.token.value && state.user.value) { - await signOut({ - skipApiCall: true, - }) - message.error(t('msg.error.youHaveBeenSignedOut')) - } + isTokenRefreshInProgress.value = true + try { + const response = await axiosInstance.post('/auth/token/refresh', null, { + withCredentials: true, + }) + if (response.data?.token) { + signIn(response.data.token, true) + return response.data.token + } + return null + } catch (e) { + if (state.token.value && state.user.value && !skipSignOut) { + await signOut({ + skipApiCall: true, }) - .finally(() => resolve(true)) - }) + message.error(t('msg.error.youHaveBeenSignedOut')) + } + return null + } finally { + isTokenRefreshInProgress.value = false + } } const loadAppInfo = async () => { diff --git a/packages/nc-gui/composables/useGlobal/types.ts b/packages/nc-gui/composables/useGlobal/types.ts index 9946ea88cb..19ccb0c655 100644 --- a/packages/nc-gui/composables/useGlobal/types.ts +++ b/packages/nc-gui/composables/useGlobal/types.ts @@ -91,8 +91,8 @@ export interface SignOutParams { export interface Actions { signOut: (signOutParams?: SignOutParams) => Promise - signIn: (token: string, keepProps?: boolean) => Promise - refreshToken: () => void + signIn: (token: string, keepProps?: boolean) => void + refreshToken: () => Promise loadAppInfo: () => void setIsMobileMode: (isMobileMode: boolean) => void navigateToProject: (params: { workspaceId?: string; baseId?: string; type?: NcProjectType; query?: any }) => void From 5fb1a702ba374ec1b35ead9f9045bdfd58cd3354 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:16 +0000 Subject: [PATCH 04/11] refactor: replace promise then with async/await --- .../nc-gui/composables/useApi/interceptors.ts | 155 +++--------------- .../nc-gui/composables/useGlobal/actions.ts | 22 ++- .../nc-gui/composables/useGlobal/index.ts | 2 +- .../nc-gui/composables/useGlobal/types.ts | 4 +- packages/nocodb/src/db/CustomKnex.ts | 12 +- .../src/services/users/users.service.ts | 8 +- 6 files changed, 61 insertions(+), 142 deletions(-) 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, From adf56979ea165fc289fcda18e8194f6502f8d140 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:18 +0000 Subject: [PATCH 05/11] refactor: use `useSharedExecutionFn` composable for refresh token --- .../nc-gui/composables/useGlobal/actions.ts | 20 +++++-------------- .../nc-gui/composables/useGlobal/types.ts | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index caa8061c77..0da1f4e2d9 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -1,10 +1,8 @@ import { getActivePinia } from 'pinia' -import { useStorage } from '@vueuse/core' import type { Actions, AppInfo, Getters, State } from './types' import type { NcProjectType } from '#imports' export function useGlobalActions(state: State, getters: Getters): Actions { - const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false) const isTokenUpdatedTab = useState('isTokenUpdatedTab', () => false) const setIsMobileMode = (isMobileMode: boolean) => { @@ -65,7 +63,7 @@ export function useGlobalActions(state: State, getters: Getters): Actions { } /** manually try to refresh token */ - const refreshToken = async ({ + const _refreshToken = async ({ axiosInstance, skipSignOut = false, }: { @@ -75,16 +73,6 @@ export function useGlobalActions(state: State, getters: Getters): 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) { @@ -108,11 +96,13 @@ export function useGlobalActions(state: State, getters: Getters): Actions { message.error(t('msg.error.youHaveBeenSignedOut')) } return null - } finally { - isTokenRefreshInProgress.value = false } } + const refreshToken = useSharedExecutionFn('refreshToken', _refreshToken, { + timeout: 10000, + }) + const loadAppInfo = async () => { try { const nuxtApp = useNuxtApp() diff --git a/packages/nc-gui/composables/useGlobal/types.ts b/packages/nc-gui/composables/useGlobal/types.ts index d82b6a2542..1ad3975705 100644 --- a/packages/nc-gui/composables/useGlobal/types.ts +++ b/packages/nc-gui/composables/useGlobal/types.ts @@ -92,7 +92,7 @@ export interface SignOutParams { export interface Actions { signOut: (signOutParams?: SignOutParams) => Promise signIn: (token: string, keepProps?: boolean) => void - refreshToken: (params: { axiosInstance?: AxiosInstance; skipLogout?: boolean }) => Promise + refreshToken: (params: { axiosInstance?: AxiosInstance; skipLogout?: boolean; cognitoOnly?: boolean }) => Promise loadAppInfo: () => void setIsMobileMode: (isMobileMode: boolean) => void navigateToProject: (params: { workspaceId?: string; baseId?: string; type?: NcProjectType; query?: any }) => void From 118731d5e48871c0a32bfbf065f45ded09414429 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 06/11] refactor: review comments --- packages/nc-gui/composables/useGlobal/actions.ts | 3 +-- packages/nc-gui/store/notification.ts | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index 0da1f4e2d9..fbd2c236d3 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -73,8 +73,6 @@ export function useGlobalActions(state: State, getters: Getters): Actions { const nuxtApp = useNuxtApp() const t = nuxtApp.vueApp.i18n.global.t - isTokenRefreshInProgress.value = true - if (!axiosInstance) { axiosInstance = nuxtApp.$api?.instance } @@ -101,6 +99,7 @@ export function useGlobalActions(state: State, getters: Getters): Actions { const refreshToken = useSharedExecutionFn('refreshToken', _refreshToken, { timeout: 10000, + storageDelay: 1000 }) const loadAppInfo = async () => { diff --git a/packages/nc-gui/store/notification.ts b/packages/nc-gui/store/notification.ts index 034e1fd817..2e3a4dc554 100644 --- a/packages/nc-gui/store/notification.ts +++ b/packages/nc-gui/store/notification.ts @@ -6,8 +6,6 @@ import { useStorage } from '@vueuse/core' const CancelToken = axios.CancelToken export const useNotification = defineStore('notificationStore', () => { - const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false) - const readNotifications = ref([]) const unreadNotifications = ref([]) @@ -195,7 +193,6 @@ export const useNotification = defineStore('notificationStore', () => { cancelTokenSource = null // wait if refresh token generation is in progress and cancel the polling after that // set a timeout of 10 seconds to avoid hanging - await until(isTokenRefreshInProgress).toMatch((v) => !v, { timeout: 10000 }) source?.cancel() } From 9aa032ce7ebf44df372adb187d9ca0f7407ba519 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 07/11] fix: null handling --- packages/nc-gui/composables/useApi/interceptors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useApi/interceptors.ts b/packages/nc-gui/composables/useApi/interceptors.ts index 44c4c8b671..b4e9914a52 100644 --- a/packages/nc-gui/composables/useApi/interceptors.ts +++ b/packages/nc-gui/composables/useApi/interceptors.ts @@ -73,7 +73,7 @@ export function addAxiosInterceptors(api: Api) { const response = await axiosInstance.request(config) return response } catch (refreshTokenError) { - if (refreshTokenError.code === 'ERR_CANCELED') { + if ((refreshTokenError as any)?.code === 'ERR_CANCELED') { return Promise.reject(refreshTokenError) } From 12b2b93a9a7a999641501e476f1cb0eb9f4c85a5 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 08/11] fix: missing error handling --- packages/nc-gui/middleware/02.auth.global.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/nc-gui/middleware/02.auth.global.ts b/packages/nc-gui/middleware/02.auth.global.ts index 550a6a8993..005f02afdb 100644 --- a/packages/nc-gui/middleware/02.auth.global.ts +++ b/packages/nc-gui/middleware/02.auth.global.ts @@ -75,8 +75,12 @@ export default defineNuxtRouteMiddleware(async (to, from) => { }) } - /** try generating access token using refresh token */ - await state.refreshToken() + try { + /** try generating access token using refresh token */ + await state.refreshToken({}) + } catch (e) { + console.info('Refresh token failed', (e as Error)?.message) + } /** if user is still not signed in, redirect to signin page */ if (!state.signedIn.value) { From e86e76002cec83aa19f0228b152b21e68aef5012 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 09/11] chore: lint --- packages/nc-gui/composables/useGlobal/actions.ts | 2 +- packages/nocodb/src/services/users/helpers.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index fbd2c236d3..46aa0a702f 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -99,7 +99,7 @@ export function useGlobalActions(state: State, getters: Getters): Actions { const refreshToken = useSharedExecutionFn('refreshToken', _refreshToken, { timeout: 10000, - storageDelay: 1000 + storageDelay: 1000, }) const loadAppInfo = async () => { diff --git a/packages/nocodb/src/services/users/helpers.ts b/packages/nocodb/src/services/users/helpers.ts index e4906b0b2e..234e26fad3 100644 --- a/packages/nocodb/src/services/users/helpers.ts +++ b/packages/nocodb/src/services/users/helpers.ts @@ -3,7 +3,7 @@ import * as jwt from 'jsonwebtoken'; import type User from '~/models/User'; import type { NcConfig } from '~/interface/config'; import type { Response } from 'express'; -import {NC_REFRESH_TOKEN_EXP_IN_DAYS} from "~/constants"; +import { NC_REFRESH_TOKEN_EXP_IN_DAYS } from '~/constants'; export function genJwt( user: User & { extra?: Record }, @@ -31,7 +31,9 @@ export function setTokenCookie(res: Response, token): void { // create http only cookie with refresh token that expires in 7 days const cookieOptions = { httpOnly: true, - expires: new Date(Date.now() + NC_REFRESH_TOKEN_EXP_IN_DAYS * 24 * 60 * 60 * 1000), + expires: new Date( + Date.now() + NC_REFRESH_TOKEN_EXP_IN_DAYS * 24 * 60 * 60 * 1000, + ), domain: process.env.NC_BASE_HOST_NAME || undefined, }; res.cookie('refresh_token', token, cookieOptions); From 3bfd6b0171ca967c1da04ae64ec2d845682f03b8 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 10/11] refactor: add null check --- packages/nc-gui/composables/useGlobal/actions.ts | 2 +- packages/nocodb/src/db/CustomKnex.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index 46aa0a702f..f8b4d4405e 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -2,7 +2,7 @@ import { getActivePinia } from 'pinia' import type { Actions, AppInfo, Getters, State } from './types' import type { NcProjectType } from '#imports' -export function useGlobalActions(state: State, getters: Getters): Actions { +export function useGlobalActions(state: State, _getters: Getters): Actions { const isTokenUpdatedTab = useState('isTokenUpdatedTab', () => false) const setIsMobileMode = (isMobileMode: boolean) => { diff --git a/packages/nocodb/src/db/CustomKnex.ts b/packages/nocodb/src/db/CustomKnex.ts index 0a012cf987..48555bcbb6 100644 --- a/packages/nocodb/src/db/CustomKnex.ts +++ b/packages/nocodb/src/db/CustomKnex.ts @@ -991,6 +991,7 @@ function parseNestedCondition(obj, qb, pKey?, table?, tableAlias?) { default: // if object handle recursively if ( + val && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val) @@ -1252,6 +1253,7 @@ function parseNestedConditionv2(obj, qb, pKey?, table?, tableAlias?) { default: // if object handle recursively if ( + val && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val) From 43b4e224dd50ffc9111794dbbb67c33b501d4be0 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 13 Nov 2024 07:30:19 +0000 Subject: [PATCH 11/11] refactor: set default refresh token expiry to 30 days --- packages/nocodb/src/constants/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nocodb/src/constants/index.ts b/packages/nocodb/src/constants/index.ts index d671871dd5..304359ee3d 100644 --- a/packages/nocodb/src/constants/index.ts +++ b/packages/nocodb/src/constants/index.ts @@ -7,7 +7,7 @@ export const NC_ATTACHMENT_FIELD_SIZE = export const NC_MAX_ATTACHMENTS_ALLOWED = +process.env['NC_MAX_ATTACHMENTS_ALLOWED'] || 10; export const NC_REFRESH_TOKEN_EXP_IN_DAYS = - parseInt(process.env.NC_REFRESH_TOKEN_EXP_IN_DAYS, 10) || 90; + parseInt(process.env.NC_REFRESH_TOKEN_EXP_IN_DAYS, 10) || 30; // throw error if user provided invalid value if (!NC_REFRESH_TOKEN_EXP_IN_DAYS || NC_REFRESH_TOKEN_EXP_IN_DAYS <= 0) {