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