mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
7.1 KiB
216 lines
7.1 KiB
import type { Api } from 'nocodb-sdk' |
|
import type { Actions } from '~/composables/useGlobal/types' |
|
import { defineNuxtRouteMiddleware, message, navigateTo, useApi, useGlobal, useRoles } from '#imports' |
|
import { extractSdkResponseErrorMsg } from '~/utils' |
|
|
|
/** |
|
* Global auth middleware |
|
* |
|
* On page transitions, this middleware checks if the target route requires authentication. |
|
* If the user is not signed in, the user is redirected to the sign in page. |
|
* If the user is signed in and attempts to access a route that does not require authentication (i.e. signin/signup pages), |
|
* the user is redirected to the home page. |
|
* |
|
* By default, we assume that auth is required |
|
* If not required, mark the page as requiresAuth: false |
|
* |
|
* @example |
|
* ``` |
|
* definePageMeta({ |
|
* requiresAuth: false |
|
* }) |
|
* ``` |
|
* |
|
* If auth should be circumvented completely mark the page as public |
|
* |
|
* @example |
|
* ``` |
|
* definePageMeta({ |
|
* public: true |
|
* }) |
|
* ``` |
|
*/ |
|
export default defineNuxtRouteMiddleware(async (to, from) => { |
|
const state = useGlobal() |
|
|
|
const { api } = useApi({ useGlobalInstance: true }) |
|
|
|
const { allRoles, loadRoles } = useRoles() |
|
|
|
await checkForRedirect() |
|
|
|
/** If baseHostname defined block home page access under subdomains, and redirect to workspace page */ |
|
if ( |
|
state.appInfo.value.baseHostName && |
|
!location.hostname?.startsWith(`${state.appInfo.value.mainSubDomain}.`) && |
|
to.path === '/' |
|
) { |
|
return navigateTo(`/${location.hostname.split('.')[0]}`) |
|
} |
|
|
|
/** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */ |
|
if (!state.signedIn.value && state.appInfo.value.googleAuthEnabled) { |
|
await tryGoogleAuth(api, state.signIn) |
|
} |
|
|
|
/** if not signedIn try token population based on short-lived-token */ |
|
if (!state.signedIn.value) await tryShortTokenAuth(api, state.signIn) |
|
|
|
/** if public allow all visitors */ |
|
if (to.meta.public) return |
|
|
|
/** if shared base allow without validating */ |
|
if (to.params.typeOrId === 'base') return |
|
|
|
/** if auth is required or unspecified (same `as required) and user is not signed in, redirect to signin page */ |
|
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) { |
|
/** If this is the first usern navigate to signup page directly */ |
|
if (state.appInfo.value.firstUser) { |
|
const query = to.fullPath !== '/' && to.fullPath.match(/^\/(?!\?)/) ? { continueAfterSignIn: to.fullPath } : {} |
|
if (query.continueAfterSignIn) { |
|
localStorage.setItem('continueAfterSignIn', query.continueAfterSignIn) |
|
} |
|
|
|
return navigateTo({ |
|
path: '/signup', |
|
query, |
|
}) |
|
} |
|
|
|
/** try generating access token using refresh token */ |
|
await state.refreshToken() |
|
|
|
/** if user is still not signed in, redirect to signin page */ |
|
if (!state.signedIn.value) { |
|
return navigateTo({ |
|
path: '/signin', |
|
query: to.fullPath !== '/' && to.fullPath.match(/^\/(?!\?)/) ? { continueAfterSignIn: to.fullPath } : {}, |
|
}) |
|
} |
|
} else if (to.meta.requiresAuth === false && state.signedIn.value) { |
|
if (to.query?.logout) { |
|
await state.signOut(true) |
|
return navigateTo('/signin') |
|
} |
|
|
|
/** |
|
* if user was turned away from non-auth page but also came from a non-auth page (e.g. user went to /signin and reloaded the page) |
|
* redirect to home page |
|
* |
|
* else redirect back to the page they were coming from |
|
*/ |
|
if (from.meta.requiresAuth === false) { |
|
return navigateTo('/') |
|
} else { |
|
return navigateTo(from.path) |
|
} |
|
} else { |
|
/** If page is limited to certain users verify the user have the roles */ |
|
if (to.meta.allowedRoles && to.meta.allowedRoles.every((role) => !allRoles.value?.[role])) { |
|
message.error("You don't have enough permission to access the page.") |
|
return navigateTo('/') |
|
} |
|
|
|
/** if users are accessing the bases without having enough permissions, redirect to My Projects page */ |
|
if (to.params.baseId && from.params.baseId !== to.params.baseId) { |
|
await loadRoles() |
|
|
|
if (state.user.value?.roles?.guest) { |
|
message.error("You don't have enough permission to access the base.") |
|
|
|
return navigateTo('/') |
|
} |
|
} |
|
} |
|
}) |
|
|
|
/** |
|
* If present, try using google auth data to sign user in before navigating to the next page |
|
*/ |
|
async function tryGoogleAuth(api: Api<any>, signIn: Actions['signIn']) { |
|
if (window.location.search && /\bscope=|\bstate=/.test(window.location.search) && /\bcode=/.test(window.location.search)) { |
|
let extraProps: any = {} |
|
try { |
|
let authProvider = 'google' |
|
if (window.location.search.includes('state=github')) { |
|
authProvider = 'github' |
|
} else if (window.location.search.includes('state=oidc')) { |
|
authProvider = 'oidc' |
|
} |
|
|
|
// `extra` prop is used in our cloud implementation, so we are keeping it |
|
const { |
|
data: { token, extra }, |
|
} = await api.instance.post(`/auth/${authProvider}/genTokenByCode${window.location.search}`) |
|
|
|
// if extra prop is null/undefined set it as an empty object as fallback |
|
extraProps = extra || {} |
|
|
|
signIn(token) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
|
|
const newURL = window.location.href.split('?')[0] |
|
window.history.pushState( |
|
'object', |
|
document.title, |
|
`${extraProps?.continueAfterSignIn ? `${newURL}#/?continueAfterSignIn=${extraProps.continueAfterSignIn}` : newURL}`, |
|
) |
|
window.location.reload() |
|
} |
|
} |
|
|
|
/** |
|
* If short-token present, try using it to generate long-living token before navigating to the next page |
|
*/ |
|
async function tryShortTokenAuth(api: Api<any>, signIn: Actions['signIn']) { |
|
if (window.location.search && /\bshort-token=/.test(window.location.search)) { |
|
let extraProps: any = {} |
|
try { |
|
// `extra` prop is used in our cloud implementation, so we are keeping it |
|
const { data } = await api.instance.post( |
|
`/auth/long-lived-token`, |
|
{}, |
|
{ |
|
headers: { |
|
'xc-short-token': window.location.search.split('=')[1], |
|
} as any, |
|
}, |
|
) |
|
|
|
const { token, extra } = data |
|
|
|
// if extra prop is null/undefined set it as an empty object as fallback |
|
extraProps = extra || {} |
|
|
|
signIn(token) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
|
|
const newURL = window.location.href.split('?')[0] |
|
window.history.pushState( |
|
'object', |
|
document.title, |
|
`${extraProps?.continueAfterSignIn ? `${newURL}#/?continueAfterSignIn=${extraProps.continueAfterSignIn}` : newURL}`, |
|
) |
|
window.location.reload() |
|
} |
|
} |
|
|
|
/** Check if url contains redirect param and redirect to the url if found */ |
|
async function checkForRedirect() { |
|
if (window.location.search && /\bui-redirect=/.test(window.location.search)) { |
|
let url |
|
try { |
|
url = decodeURIComponent(window.location.search.split('=')[1]) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
|
|
const newURL = window.location.href.split('?')[0] |
|
window.history.pushState('object', document.title, `${newURL}#${url}`) |
|
window.location.reload() |
|
} |
|
}
|
|
|