From b7db5ea3a6bc50fd14748e3cf200beab471b42fd Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sat, 30 Jan 2021 05:45:24 +0100 Subject: [PATCH] [Vue3][Quasar2] Fixed boot files and UserSessionStore --- quasar.conf.js | 2 +- src/boot/axios.ts | 74 ++++++++++++++++--------------- src/boot/filter.ts | 10 ----- src/boot/loading.ts | 2 +- src/boot/login.ts | 11 +++-- src/boot/notify.ts | 2 +- src/boot/plugins.ts | 6 +-- src/plugins/user/store/index.ts | 7 +++ src/plugins/user/store/session.ts | 72 ++++++++++++++++-------------- src/plugins/user/store/user.ts | 36 +++++++-------- src/store/store-flag.d.ts | 9 ++++ 11 files changed, 123 insertions(+), 108 deletions(-) delete mode 100644 src/boot/filter.ts create mode 100644 src/plugins/user/store/index.ts create mode 100644 src/store/store-flag.d.ts diff --git a/quasar.conf.js b/quasar.conf.js index 86b2529..24abfa1 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -21,7 +21,7 @@ module.exports = configure(function (/* ctx */) { files: './src/**/*.{ts,tsx,js,jsx,vue}', }, } - } + }, // https://quasar.dev/quasar-cli/prefetch-feature // preFetch: true, diff --git a/src/boot/axios.ts b/src/boot/axios.ts index 6ffca27..99c8bd4 100644 --- a/src/boot/axios.ts +++ b/src/boot/axios.ts @@ -1,45 +1,25 @@ -import axios, { AxiosInstance, AxiosError } from 'axios'; +import config from 'src/config'; import { boot } from 'quasar/wrappers'; -import config from '../config'; -import { Store } from 'vuex'; -import { StateInterface } from 'src/store'; -import { LocalStorage } from 'quasar'; -import { Notify } from 'quasar'; +import { LocalStorage, Notify } from 'quasar'; +import axios, { AxiosError, AxiosInstance } from 'axios'; +import { UserSessionState } from 'src/plugins/user/store'; -declare module 'vue/types/vue' { - interface Vue { +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { $axios: AxiosInstance; } } -export const setBaseUrl = (url: string) => { - LocalStorage.set('baseURL', url); - axios.defaults.baseURL = url; - Notify.create({ - message: 'Serveraddresse gespeichert', - position: 'bottom', - caption: `${url}`, - color: 'positive', - }); - setTimeout(() => { - window.location.reload(); - }, 5000); -}; +const api = axios.create({ + baseURL: LocalStorage.getItem('baseURL') || config.baseURL, +}); -export default boot>(({ Vue, store, router }) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - Vue.prototype.$axios = axios; - const baseURL = LocalStorage.getItem('baseURL'); - if (baseURL) { - axios.defaults.baseURL = baseURL; - } else { - axios.defaults.baseURL = config.baseURL; - } +export default boot(({ app, store, router }) => { /*** * Intercept requests and insert Token if available */ - axios.interceptors.request.use((config) => { - const session = store.state.session.currentSession; + api.interceptors.request.use((config) => { + const session = store.state.sessions.currentSession; if (session?.token) { config.headers = { Authorization: 'Bearer ' + session.token }; } @@ -62,15 +42,39 @@ export default boot>(({ Vue, store, router }) => { ) { return router.push({ name: 'offline', - query: { redirect: router.currentRoute.fullPath }, + query: { redirect: router.currentRoute.value.fullPath }, }); } else if (e.response && e.response.status == 401) { - if (router.currentRoute.name !== 'login') return store.dispatch('session/clearCurrent'); + if (router.currentRoute.value.name !== 'login') + return store.dispatch('session/clearCurrent'); } } return Promise.reject(error); } ); + // for use inside Vue files (Options API) through this.$axios and this.$api + + app.config.globalProperties.$axios = axios; + // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form) + // so you won't necessarily have to import axios in each vue file + + app.config.globalProperties.$api = api; + // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) + // so you can easily perform requests against your app's API }); -export { axios }; +export { axios, api }; + +export const setBaseUrl = (url: string) => { + LocalStorage.set('baseURL', url); + axios.defaults.baseURL = url; + Notify.create({ + message: 'Serveraddresse gespeichert', + position: 'bottom', + caption: `${url}`, + color: 'positive', + }); + setTimeout(() => { + window.location.reload(); + }, 5000); +}; diff --git a/src/boot/filter.ts b/src/boot/filter.ts deleted file mode 100644 index f97513a..0000000 --- a/src/boot/filter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { boot } from 'quasar/wrappers'; -import { formatDateTime } from 'src/utils/datetime'; - -export default boot(({ Vue }) => { - Vue.filter('date', formatDateTime); - Vue.filter('time', (date: Date, seconds = false) => formatDateTime(date, false, true, seconds)); - Vue.filter('dateTime', (date: Date, seconds = false, weekday = false) => - formatDateTime(date, true, true, seconds, weekday) - ); -}); diff --git a/src/boot/loading.ts b/src/boot/loading.ts index 53f6d46..56b4e14 100644 --- a/src/boot/loading.ts +++ b/src/boot/loading.ts @@ -4,7 +4,7 @@ import DarkCircularProgress from 'components/loading/DarkCircularProgress.vue'; // "async" is optional; // more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file -export default boot((/* { app, router, Vue ... } */) => { +export default boot(() => { Loading.setDefaults({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/src/boot/login.ts b/src/boot/login.ts index cc33679..e6a9ab0 100644 --- a/src/boot/login.ts +++ b/src/boot/login.ts @@ -1,11 +1,10 @@ import { boot } from 'quasar/wrappers'; -import { StateInterface } from 'src/store'; +import { UserSessionState } from 'src/plugins/user/store'; import { RouteRecord } from 'vue-router'; -import { Store } from 'vuex'; -export default boot>(({ router, store }) => { +export default boot(({ router, store }) => { router.beforeEach((to, from, next) => { - const session = store.state.session.currentSession; + const session = store.state.sessions.currentSession; if (to.path == from.path) { return; @@ -29,7 +28,7 @@ export default boot>(({ router, store }) => { if ((<{ permissions: FG.Permission[] }>record.meta).permissions) { return (<{ permissions: FG.Permission[] }>record.meta).permissions.every( (permission: string) => { - return store.state.user.currentPermissions.includes(permission); + return store.state.users.currentPermissions.includes(permission); } ); } @@ -41,7 +40,7 @@ export default boot>(({ router, store }) => { next({ name: 'login', query: { redirect: to.fullPath } }); } } else { - if (to.name == 'login' && store.state.user.currentUser && !to.params['logout']) { + if (to.name == 'login' && store.state.users.currentUser && !to.params['logout']) { // Called login while already logged in void next({ name: 'dashboard' }); } else { diff --git a/src/boot/notify.ts b/src/boot/notify.ts index 8dfc333..28f7609 100644 --- a/src/boot/notify.ts +++ b/src/boot/notify.ts @@ -3,7 +3,7 @@ import { Notify } from 'quasar'; // "async" is optional; // more info on params: https://quasar.dev/quasar-cli/boot-files -export default boot((/* { app, router, Vue ... } */) => { +export default boot(() => { Notify.registerType('error', { color: 'negative', icon: 'mdi-alert-circle', diff --git a/src/boot/plugins.ts b/src/boot/plugins.ts index caf64f5..bda6085 100644 --- a/src/boot/plugins.ts +++ b/src/boot/plugins.ts @@ -1,11 +1,11 @@ import { boot } from 'quasar/wrappers'; import { Store } from 'vuex'; -import { StateInterface } from 'src/store'; import { FG_Plugin } from 'src/plugins'; import routes from 'src/router/routes'; import { axios } from 'boot/axios'; import { AxiosResponse } from 'axios'; import { Router, RouteRecordRaw } from 'vue-router'; +import { UserSessionState } from 'src/plugins/user/store'; const config = { // Do not change required Modules !! @@ -154,7 +154,7 @@ function loadPlugin( modules: string[], backendpromise: Promise, plugins: FG_Plugin.Plugin[], - store: Store, + store: Store, router: Router ): FG_Plugin.LoadedPlugins { modules.forEach((requiredModule) => { @@ -211,7 +211,7 @@ async function getBackend(): Promise { // "async" is optional; // more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file -export default boot>(({ router, store, app }) => { +export default boot(({ router, store, app }) => { const plugins: FG_Plugin.Plugin[] = []; const backendPromise = getBackend(); diff --git a/src/plugins/user/store/index.ts b/src/plugins/user/store/index.ts new file mode 100644 index 0000000..c20b992 --- /dev/null +++ b/src/plugins/user/store/index.ts @@ -0,0 +1,7 @@ +import { SessionStateInterface } from './session'; +import { UserStateInterface } from './user'; + +export interface UserSessionState { + sessions: SessionStateInterface; + users: UserStateInterface; +} diff --git a/src/plugins/user/store/session.ts b/src/plugins/user/store/session.ts index d6b21ad..eb357a3 100644 --- a/src/plugins/user/store/session.ts +++ b/src/plugins/user/store/session.ts @@ -1,12 +1,12 @@ import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'; import { LoginData, LoginResponse } from 'src/plugins/user/models'; -import { StateInterface } from 'src/store'; -import { axios } from 'src/boot/axios'; +import { api } from 'src/boot/axios'; import { AxiosError, AxiosResponse } from 'axios'; -import { Router } from 'src/router'; import { LocalStorage } from 'quasar'; +import { UserSessionState } from '.'; +import { useRouter } from 'vue-router'; -export interface SessionInterface { +export interface SessionStateInterface { currentSession?: FG.Session; sessions: FG.Session[]; loading: boolean; @@ -22,13 +22,13 @@ function loadCurrentSession() { return session; } -const state: SessionInterface = { +const state: SessionStateInterface = { sessions: [], currentSession: loadCurrentSession() || undefined, loading: false, }; -const mutations: MutationTree = { +const mutations: MutationTree = { setCurrentSession(state, session: FG.Session) { LocalStorage.set('currentSession', session); state.currentSession = session; @@ -51,19 +51,22 @@ const mutations: MutationTree = { }, }; -const actions: ActionTree = { +const actions: ActionTree = { /** Used to authenticate the user * Setting current Session, User and Permissions. * @param param0 Context * @param data Credentitals */ - login({ commit, dispatch }, data: LoginData) { - return axios + login({ commit }, data: LoginData) { + return api .post('/auth', data) - .then(async (response: AxiosResponse) => { - response.data.expires = new Date(response.data.expires); - commit('setCurrentSession', response.data); - await dispatch('user/getCurrentUser', undefined, { root: true }); + .then((response: AxiosResponse) => { + response.data.session.expires = new Date(response.data.session.expires); + commit('setCurrentSession', response.data.session); + commit('user/setCurrentUser', response.data.user, { root: true }); + commit('user/setCurrentPermissions', response.data.permissions, { + root: true, + }); }) .catch((error: AxiosError) => { return Promise.reject(error.response); @@ -74,8 +77,8 @@ const actions: ActionTree = { * Alias of deleteSession with current session as target */ logout({ dispatch, rootState }) { - if (rootState.session.currentSession) { - dispatch('deleteSession', rootState.session.currentSession.token).catch((error) => { + if (rootState.sessions.currentSession) { + dispatch('deleteSession', rootState.sessions.currentSession.token).catch((error) => { console.log(error); void dispatch('clearCurrent', false); }); @@ -88,10 +91,10 @@ const actions: ActionTree = { */ deleteSession({ commit, dispatch, rootState }, token: string) { commit('setLoading', true); - axios + api .delete(`/auth/${token}`) .then(() => { - if (token === rootState.session.currentSession?.token) { + if (token === rootState.sessions.currentSession?.token) { void dispatch('clearCurrent', false); } else { dispatch('getSessions').catch((error) => { @@ -110,23 +113,26 @@ const actions: ActionTree = { * Clear current session and logged in user */ clearCurrent({ commit }, redirect = true) { - void Router.push({ - name: 'login', - query: redirect ? { redirect: Router.currentRoute.fullPath } : {}, - params: { logout: 'true' }, - }).then(() => { - commit('clearCurrentSession'); - commit('user/clearCurrentUser', null, { root: true }); - // ensure also volatile store gets cleared by refreshing the site - Router.go(0); - }); + const router = useRouter(); + void router + .push({ + name: 'login', + query: redirect ? { redirect: router.currentRoute.value.fullPath } : {}, + params: { logout: 'true' }, + }) + .then(() => { + commit('clearCurrentSession'); + commit('user/clearCurrentUser', null, { root: true }); + // ensure also volatile store gets cleared by refreshing the site + router.go(0); + }); }, /** * Get all sessions from current User */ getSessions({ commit, state }) { commit('setLoading', true); - axios + api .get('/auth') .then((response: AxiosResponse) => { response.data.forEach((session) => { @@ -149,7 +155,7 @@ const actions: ActionTree = { }, updateSession({ commit, state }, data: { lifetime: number; token: string }) { commit('setLoading', true); - axios + api .put(`auth/${data.token}`, { value: data.lifetime }) .then((response: AxiosResponse) => { response.data.expires = new Date(response.data.expires); @@ -164,16 +170,16 @@ const actions: ActionTree = { console.log('updateSession', data); }, requestPasswordReset({}, data) { - return axios.post('/auth/reset', data); + return api.post('/auth/reset', data); }, resetPassword({}, data) { - return axios.post('/auth/reset', data).catch((error: AxiosError) => { + return api.post('/auth/reset', data).catch((error: AxiosError) => { return Promise.reject(error.response); }); }, }; -const getters: GetterTree = { +const getters: GetterTree = { currentSession(state) { return state.currentSession; }, @@ -185,7 +191,7 @@ const getters: GetterTree = { }, }; -const sessions: Module = { +const sessions: Module = { namespaced: true, state, mutations, diff --git a/src/plugins/user/store/user.ts b/src/plugins/user/store/user.ts index e06d4f1..0f7d22c 100644 --- a/src/plugins/user/store/user.ts +++ b/src/plugins/user/store/user.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'; -import { StateInterface } from 'src/store'; -import { axios } from 'boot/axios'; +import { api } from 'boot/axios'; import { AxiosError, AxiosResponse } from 'axios'; import { SessionStorage } from 'quasar'; import { CurrentUserResponse } from 'src/plugins/user/models'; +import { UserSessionState } from '.'; export interface UserStateInterface { currentUser?: FG.User; @@ -76,12 +76,12 @@ const mutations: MutationTree = { }, }; -const actions: ActionTree = { +const actions: ActionTree = { getCurrentUser({ commit, rootState }) { - if (rootState.session.currentSession) { + if (rootState.sessions.currentSession) { commit('setLoading'); - axios - .get(`/users/${rootState.session.currentSession.userid}`) + api + .get(`/users/${rootState.sessions.currentSession.userid}`) .then((response: AxiosResponse) => { commit('setCurrentUser', response.data); commit('setCurrentPermissions', response.data.permissions); @@ -99,7 +99,7 @@ const actions: ActionTree = { getUsers({ commit }) { commit('setLoading'); - axios + api .get('/users') .then((response: AxiosResponse) => { response.data.forEach((user) => { @@ -117,7 +117,7 @@ const actions: ActionTree = { updateUser({ commit, state, dispatch }, data: FG.User) { commit('setLoading'); - axios + api .put(`/users/${data.userid}`, data) .then(() => { if (state.currentUser && state.currentUser.userid === data.userid) @@ -136,7 +136,7 @@ const actions: ActionTree = { commit('setLoading'); const formData = new FormData(); formData.append('file', payload.file); - return axios + return api .post(`/users/${payload.user.userid}/avatar`, formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -152,7 +152,7 @@ const actions: ActionTree = { setUser({ commit, state, dispatch }, data: FG.User) { commit('setLoading'); - axios + api .post('users', data) .then(() => { if (state.currentUser && state.currentUser.userid === data.userid) @@ -170,7 +170,7 @@ const actions: ActionTree = { getRoles({ commit, state }, force = false) { if (!force && state.roles.length > 0) return; commit('setLoading'); - axios + api .get('/roles') .then((response: AxiosResponse) => { commit('setRoles', response.data); @@ -180,7 +180,7 @@ const actions: ActionTree = { updateRole({ commit }, data: FG.Role) { commit('setLoading'); - axios + api .put(`/roles/${data.id}`, data) .then(() => { commit('updateRole', data); @@ -192,7 +192,7 @@ const actions: ActionTree = { newRole({ commit }, data: FG.Role) { commit('setLoading'); - return axios + return api .post('/roles', data) .then((response: AxiosResponse) => { commit('addRole', response.data); @@ -205,7 +205,7 @@ const actions: ActionTree = { deleteRole({ commit, state }, data: FG.Role) { commit('setLoading'); - axios + api .delete(`/roles/${data.id}`) .then(() => { commit( @@ -221,7 +221,7 @@ const actions: ActionTree = { getPermissions({ commit, state }, force = false) { if (!force && state.permissions.length > 0) return; commit('setLoading'); - axios + api .get('/roles/permissions') .then((response: AxiosResponse) => { commit('setPermissions', response.data); @@ -232,7 +232,7 @@ const actions: ActionTree = { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const user = getters['getUser'](data.userid); if (user === undefined || data.force === true) { - return axios.get(`/users/${data.userid}`).then((response: AxiosResponse) => { + return api.get(`/users/${data.userid}`).then((response: AxiosResponse) => { commit('setUser', response.data); return response.data; }); @@ -242,7 +242,7 @@ const actions: ActionTree = { }, }; -const getters: GetterTree = { +const getters: GetterTree = { getUser: (state) => (userid: string) => { const user = state.users.filter((usr) => usr.userid === userid); return user.length > 0 ? user[0] : undefined; @@ -264,7 +264,7 @@ const getters: GetterTree = { }, }; -const userStore: Module = { +const userStore: Module = { namespaced: true, actions, getters, diff --git a/src/store/store-flag.d.ts b/src/store/store-flag.d.ts new file mode 100644 index 0000000..af80dbe --- /dev/null +++ b/src/store/store-flag.d.ts @@ -0,0 +1,9 @@ +// THIS FEATURE-FLAG FILE IS AUTOGENERATED, +// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING +import 'quasar/dist/types/feature-flag'; + +declare module 'quasar/dist/types/feature-flag' { + interface QuasarFeatureFlags { + store: true; + } +}