mirror of https://github.com/nocodb/nocodb
Browse Source
# What's changed? * `useGlobal` replaces `useGlobalState` * split into actions, getters, state * injects into nuxt app on init (regardless where it's called)pull/2877/head
braks
2 years ago
8 changed files with 228 additions and 177 deletions
@ -0,0 +1,54 @@ |
|||||||
|
import { notification } from 'ant-design-vue' |
||||||
|
import type { UseGlobalStateReturn } from './state' |
||||||
|
import type { Actions } from '~/lib' |
||||||
|
import { useNuxtApp } from '#imports' |
||||||
|
|
||||||
|
export function useGlobalActions(state: UseGlobalStateReturn) { |
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
/** Actions */ |
||||||
|
/** Sign out by deleting the token from localStorage */ |
||||||
|
const signOut: Actions['signOut'] = () => { |
||||||
|
state.token.value = null |
||||||
|
state.user.value = null |
||||||
|
} |
||||||
|
|
||||||
|
/** Sign in by setting the token in localStorage */ |
||||||
|
const signIn: Actions['signIn'] = async (newToken) => { |
||||||
|
state.token.value = newToken |
||||||
|
|
||||||
|
if (state.payload.value) { |
||||||
|
state.user.value = { |
||||||
|
id: state.payload.value.id, |
||||||
|
email: state.payload.value.email, |
||||||
|
firstname: state.payload.value.firstname, |
||||||
|
lastname: state.payload.value.lastname, |
||||||
|
roles: state.payload.value.roles, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** manually try to refresh token */ |
||||||
|
const refreshToken = async () => { |
||||||
|
$api.instance |
||||||
|
.post('/auth/refresh-token', null, { |
||||||
|
withCredentials: true, |
||||||
|
}) |
||||||
|
.then((response) => { |
||||||
|
if (response.data?.token) { |
||||||
|
signIn(response.data.token) |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
notification.error({ |
||||||
|
// todo: add translation
|
||||||
|
message: err.message || 'You have been signed out.', |
||||||
|
}) |
||||||
|
console.error(err) |
||||||
|
|
||||||
|
signOut() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return { signIn, signOut, refreshToken } |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
import type { UseGlobalStateReturn } from './state' |
||||||
|
import type { Getters } from '~/lib' |
||||||
|
import { computed } from '#imports' |
||||||
|
|
||||||
|
export function useGlobalGetters(state: UseGlobalStateReturn) { |
||||||
|
/** Getters */ |
||||||
|
/** Verify that a user is signed in by checking if token exists and is not expired */ |
||||||
|
const signedIn: Getters['signedIn'] = computed( |
||||||
|
() => |
||||||
|
!!( |
||||||
|
!!state.token && |
||||||
|
state.token.value !== '' && |
||||||
|
state.payload.value && |
||||||
|
state.payload.value.exp && |
||||||
|
state.payload.value.exp > state.timestamp.value / 1000 |
||||||
|
), |
||||||
|
) |
||||||
|
|
||||||
|
return { signedIn } |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
import { useGlobalState } from './state' |
||||||
|
import { useGlobalActions } from './actions' |
||||||
|
import { toRefs, useNuxtApp, watch } from '#imports' |
||||||
|
import type { GlobalState } from '~/lib' |
||||||
|
import { useGlobalGetters } from '~/composables/useGlobal/getters' |
||||||
|
|
||||||
|
/** |
||||||
|
* Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`). |
||||||
|
* Manual initialization is unnecessary and should be avoided. |
||||||
|
* |
||||||
|
* The state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab. |
||||||
|
* |
||||||
|
* @example |
||||||
|
* ```js
|
||||||
|
* import { useNuxtApp } from '#app' |
||||||
|
* |
||||||
|
* const { $state } = useNuxtApp() |
||||||
|
* |
||||||
|
* const token = $state.token.value |
||||||
|
* const user = $state.user.value |
||||||
|
* ``` |
||||||
|
*/ |
||||||
|
export const useGlobal = (): GlobalState => { |
||||||
|
const { $state, provide } = useNuxtApp() |
||||||
|
|
||||||
|
if ($state) { |
||||||
|
console.warn( |
||||||
|
'[useGlobalState] Global state is injected by state plugin. Manual initialization is unnecessary and should be avoided.', |
||||||
|
) |
||||||
|
return $state |
||||||
|
} |
||||||
|
|
||||||
|
const state = $(useGlobalState()) |
||||||
|
|
||||||
|
const getters = useGlobalGetters($$(state)) |
||||||
|
|
||||||
|
const actions = useGlobalActions($$(state)) |
||||||
|
|
||||||
|
/** try to refresh token before expiry (5 min before expiry) */ |
||||||
|
watch( |
||||||
|
() => !!(state.payload && state.payload.exp && state.payload.exp - 5 * 60 < state.timestamp / 1000), |
||||||
|
async (expiring) => { |
||||||
|
if (getters.signedIn.value && state.payload && expiring) { |
||||||
|
await actions.refreshToken() |
||||||
|
} |
||||||
|
}, |
||||||
|
{ immediate: true }, |
||||||
|
) |
||||||
|
|
||||||
|
provide('state', state) |
||||||
|
|
||||||
|
return { ...toRefs(state), ...getters, ...actions } |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
import { usePreferredLanguages, useStorage } from '@vueuse/core' |
||||||
|
import { useJwt } from '@vueuse/integrations/useJwt' |
||||||
|
import type { JwtPayload } from 'jwt-decode' |
||||||
|
import type { ComputedRef, Ref, ToRefs } from 'vue' |
||||||
|
import type { WritableComputedRef } from '@vue/reactivity' |
||||||
|
import { computed, ref, toRefs, useNuxtApp, useTimestamp } from '#imports' |
||||||
|
import type { StoredState, User } from '~/lib' |
||||||
|
|
||||||
|
const storageKey = 'nocodb-gui-v2' |
||||||
|
|
||||||
|
export type UseGlobalStateReturn = ToRefs<StoredState> & { |
||||||
|
token: WritableComputedRef<string | null> |
||||||
|
payload: ComputedRef<(JwtPayload & User) | null> |
||||||
|
sidebarOpen: Ref<boolean> |
||||||
|
timestamp: Ref<number> |
||||||
|
} |
||||||
|
|
||||||
|
export function useGlobalState(): UseGlobalStateReturn { |
||||||
|
/** get the preferred languages of a user, according to browser settings */ |
||||||
|
const preferredLanguages = $(usePreferredLanguages()) |
||||||
|
/** todo: reimplement; get the preferred dark mode setting, according to browser settings */ |
||||||
|
// const prefersDarkMode = $(usePreferredDark())
|
||||||
|
const prefersDarkMode = false |
||||||
|
|
||||||
|
const { |
||||||
|
vueApp: { i18n }, |
||||||
|
} = useNuxtApp() |
||||||
|
|
||||||
|
/** |
||||||
|
* Set initial language based on browser settings. |
||||||
|
* If the user has not set a preferred language, we fallback to 'en'. |
||||||
|
* If the user has set a preferred language, we try to find a matching locale in the available locales. |
||||||
|
*/ |
||||||
|
const preferredLanguage = preferredLanguages.reduce<string>((locale, language) => { |
||||||
|
/** split language to language and code, e.g. en-GB -> [en, GB] */ |
||||||
|
const [lang, code] = language.split(/[_-]/) |
||||||
|
|
||||||
|
/** find all locales that match the language */ |
||||||
|
let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang)) |
||||||
|
|
||||||
|
/** If we can match more than one locale, we check if the code of the language matches as well */ |
||||||
|
if (availableLocales.length > 1) { |
||||||
|
availableLocales = availableLocales.filter((locale) => locale.endsWith(code)) |
||||||
|
} |
||||||
|
|
||||||
|
/** if there are still multiple locales, pick the first one */ |
||||||
|
const availableLocale = availableLocales[0] |
||||||
|
|
||||||
|
/** if we found a matching locale, return it */ |
||||||
|
if (availableLocale) locale = availableLocale |
||||||
|
|
||||||
|
return locale |
||||||
|
}, 'en' /** fallback locale */) |
||||||
|
|
||||||
|
/** State */ |
||||||
|
const initialState: StoredState = { token: null, user: null, lang: preferredLanguage, darkMode: prefersDarkMode } |
||||||
|
|
||||||
|
/** saves a reactive state, any change to these values will write/delete to localStorage */ |
||||||
|
const storage = useStorage<StoredState>(storageKey, initialState) |
||||||
|
|
||||||
|
/** force turn off of dark mode, regardless of previously stored settings */ |
||||||
|
storage.value.darkMode = false |
||||||
|
|
||||||
|
/** current token ref, used by `useJwt` to reactively parse our token payload */ |
||||||
|
const token = computed({ |
||||||
|
get: () => storage.value.token || '', |
||||||
|
set: (val) => (storage.value.token = val), |
||||||
|
}) |
||||||
|
|
||||||
|
/** reactive token payload */ |
||||||
|
const { payload } = useJwt<JwtPayload & User>(token.value) |
||||||
|
|
||||||
|
/** is sidebar open */ |
||||||
|
const sidebarOpen = ref(false) |
||||||
|
|
||||||
|
/** reactive timestamp to check token expiry against */ |
||||||
|
const timestamp = useTimestamp({ immediate: true, interval: 100 }) |
||||||
|
|
||||||
|
return { |
||||||
|
...(toRefs(storage) as any), |
||||||
|
token, |
||||||
|
payload, |
||||||
|
sidebarOpen, |
||||||
|
timestamp, |
||||||
|
} |
||||||
|
} |
@ -1,169 +0,0 @@ |
|||||||
import { breakpointsTailwind, usePreferredLanguages, useStorage } from '@vueuse/core' |
|
||||||
import { useJwt } from '@vueuse/integrations/useJwt' |
|
||||||
import type { JwtPayload } from 'jwt-decode' |
|
||||||
import { notification } from 'ant-design-vue' |
|
||||||
import initialFeedBackForm from './initialFeedbackForm' |
|
||||||
import { computed, ref, toRefs, useBreakpoints, useNuxtApp, useTimestamp, watch } from '#imports' |
|
||||||
import type { Actions, Getters, GlobalState, StoredState, User } from '~/lib/types' |
|
||||||
|
|
||||||
const storageKey = 'nocodb-gui-v2' |
|
||||||
|
|
||||||
/** |
|
||||||
* Global state is injected by {@link import('~/plugins/state') state} plugin into our nuxt app (available as `$state`). |
|
||||||
* Manual initialization is unnecessary and should be avoided. |
|
||||||
* |
|
||||||
* The state is stored in {@link WindowLocalStorage localStorage}, so it will be available even if the user closes the browser tab. |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```js
|
|
||||||
* import { useNuxtApp } from '#app' |
|
||||||
* |
|
||||||
* const { $state } = useNuxtApp() |
|
||||||
* |
|
||||||
* const token = $state.token.value |
|
||||||
* const user = $state.user.value |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export const useGlobalState = (): GlobalState => { |
|
||||||
const { $state } = useNuxtApp() |
|
||||||
|
|
||||||
if ($state) { |
|
||||||
console.warn( |
|
||||||
'[useGlobalState] Global state is injected by state plugin. Manual initialization is unnecessary and should be avoided.', |
|
||||||
) |
|
||||||
return $state |
|
||||||
} |
|
||||||
|
|
||||||
/** get the preferred languages of a user, according to browser settings */ |
|
||||||
const preferredLanguages = $(usePreferredLanguages()) |
|
||||||
/** todo: reimplement; get the preferred dark mode setting, according to browser settings */ |
|
||||||
// const prefersDarkMode = $(usePreferredDark())
|
|
||||||
const prefersDarkMode = false |
|
||||||
|
|
||||||
/** get current breakpoints (for enabling sidebar) */ |
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind) |
|
||||||
|
|
||||||
/** reactive timestamp to check token expiry against */ |
|
||||||
const timestamp = $(useTimestamp({ immediate: true, interval: 100 })) |
|
||||||
|
|
||||||
const { |
|
||||||
$api, |
|
||||||
vueApp: { i18n }, |
|
||||||
} = useNuxtApp() |
|
||||||
|
|
||||||
/** |
|
||||||
* Set initial language based on browser settings. |
|
||||||
* If the user has not set a preferred language, we fallback to 'en'. |
|
||||||
* If the user has set a preferred language, we try to find a matching locale in the available locales. |
|
||||||
*/ |
|
||||||
const preferredLanguage = preferredLanguages.reduce<string>((locale, language) => { |
|
||||||
/** split language to language and code, e.g. en-GB -> [en, GB] */ |
|
||||||
const [lang, code] = language.split(/[_-]/) |
|
||||||
|
|
||||||
/** find all locales that match the language */ |
|
||||||
let availableLocales = i18n.availableLocales.filter((locale) => locale.startsWith(lang)) |
|
||||||
|
|
||||||
/** If we can match more than one locale, we check if the code of the language matches as well */ |
|
||||||
if (availableLocales.length > 1) { |
|
||||||
availableLocales = availableLocales.filter((locale) => locale.endsWith(code)) |
|
||||||
} |
|
||||||
|
|
||||||
/** if there are still multiple locales, pick the first one */ |
|
||||||
const availableLocale = availableLocales[0] |
|
||||||
|
|
||||||
/** if we found a matching locale, return it */ |
|
||||||
if (availableLocale) locale = availableLocale |
|
||||||
|
|
||||||
return locale |
|
||||||
}, 'en' /** fallback locale */) |
|
||||||
|
|
||||||
/** State */ |
|
||||||
const initialState: StoredState = { |
|
||||||
token: null, |
|
||||||
user: null, |
|
||||||
lang: preferredLanguage, |
|
||||||
darkMode: prefersDarkMode, |
|
||||||
feedbackForm: initialFeedBackForm, |
|
||||||
} |
|
||||||
|
|
||||||
/** saves a reactive state, any change to these values will write/delete to localStorage */ |
|
||||||
const storage = $(useStorage<StoredState>(storageKey, initialState)) |
|
||||||
|
|
||||||
/** force turn off of dark mode, regardless of previously stored settings */ |
|
||||||
storage.darkMode = false |
|
||||||
|
|
||||||
/** current token ref, used by `useJwt` to reactively parse our token payload */ |
|
||||||
let token = $computed({ |
|
||||||
get: () => storage.token || '', |
|
||||||
set: (val) => (storage.token = val), |
|
||||||
}) |
|
||||||
|
|
||||||
/** reactive token payload */ |
|
||||||
const { payload } = $(useJwt<JwtPayload & User>($$(token!))) |
|
||||||
|
|
||||||
/** Getters */ |
|
||||||
/** Verify that a user is signed in by checking if token exists and is not expired */ |
|
||||||
const signedIn: Getters['signedIn'] = computed( |
|
||||||
() => !!(!!token && token !== '' && payload && payload.exp && payload.exp > timestamp / 1000), |
|
||||||
) |
|
||||||
|
|
||||||
/** is sidebar open */ |
|
||||||
const sidebarOpen = ref(signedIn.value && breakpoints.greater('md').value) |
|
||||||
|
|
||||||
/** Actions */ |
|
||||||
/** Sign out by deleting the token from localStorage */ |
|
||||||
const signOut: Actions['signOut'] = () => { |
|
||||||
storage.token = null |
|
||||||
storage.user = null |
|
||||||
} |
|
||||||
|
|
||||||
/** Sign in by setting the token in localStorage */ |
|
||||||
const signIn: Actions['signIn'] = async (newToken) => { |
|
||||||
token = newToken |
|
||||||
|
|
||||||
if (payload) { |
|
||||||
storage.user = { |
|
||||||
id: payload.id, |
|
||||||
email: payload.email, |
|
||||||
firstname: payload.firstname, |
|
||||||
lastname: payload.lastname, |
|
||||||
roles: payload.roles, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** manually try to refresh token */ |
|
||||||
const refreshToken = async () => { |
|
||||||
$api.instance |
|
||||||
.post('/auth/refresh-token', null, { |
|
||||||
withCredentials: true, |
|
||||||
}) |
|
||||||
.then((response) => { |
|
||||||
if (response.data?.token) { |
|
||||||
signIn(response.data.token) |
|
||||||
} |
|
||||||
}) |
|
||||||
.catch((err) => { |
|
||||||
notification.error({ |
|
||||||
// todo: add translation
|
|
||||||
message: err.message || 'You have been signed out.', |
|
||||||
}) |
|
||||||
console.error(err) |
|
||||||
|
|
||||||
signOut() |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** try to refresh token before expiry (5 min before expiry) */ |
|
||||||
watch( |
|
||||||
() => !!(payload && payload.exp && payload.exp - 5 * 60 < timestamp / 1000), |
|
||||||
async (expiring) => { |
|
||||||
if (signedIn.value && payload && expiring) { |
|
||||||
await refreshToken() |
|
||||||
} |
|
||||||
}, |
|
||||||
{ immediate: true }, |
|
||||||
) |
|
||||||
|
|
||||||
return { ...toRefs(storage), signedIn, signOut, signIn, sidebarOpen } |
|
||||||
} |
|
Loading…
Reference in new issue