release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
11 changed files with 123 additions and 108 deletions
Showing only changes of commit b7db5ea3a6 - Show all commits

View File

@ -21,7 +21,7 @@ module.exports = configure(function (/* ctx */) {
files: './src/**/*.{ts,tsx,js,jsx,vue}', files: './src/**/*.{ts,tsx,js,jsx,vue}',
}, },
} }
} },
// https://quasar.dev/quasar-cli/prefetch-feature // https://quasar.dev/quasar-cli/prefetch-feature
// preFetch: true, // preFetch: true,

View File

@ -1,45 +1,25 @@
import axios, { AxiosInstance, AxiosError } from 'axios'; import config from 'src/config';
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import config from '../config'; import { LocalStorage, Notify } from 'quasar';
import { Store } from 'vuex'; import axios, { AxiosError, AxiosInstance } from 'axios';
import { StateInterface } from 'src/store'; import { UserSessionState } from 'src/plugins/user/store';
import { LocalStorage } from 'quasar';
import { Notify } from 'quasar';
declare module 'vue/types/vue' { declare module '@vue/runtime-core' {
interface Vue { interface ComponentCustomProperties {
$axios: AxiosInstance; $axios: AxiosInstance;
} }
} }
export const setBaseUrl = (url: string) => { const api = axios.create({
LocalStorage.set('baseURL', url); baseURL: <string | undefined>LocalStorage.getItem('baseURL') || config.baseURL,
axios.defaults.baseURL = url; });
Notify.create({
message: 'Serveraddresse gespeichert',
position: 'bottom',
caption: `${url}`,
color: 'positive',
});
setTimeout(() => {
window.location.reload();
}, 5000);
};
export default boot<Store<StateInterface>>(({ Vue, store, router }) => { export default boot<UserSessionState>(({ app, store, router }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
Vue.prototype.$axios = axios;
const baseURL = <string | undefined>LocalStorage.getItem('baseURL');
if (baseURL) {
axios.defaults.baseURL = baseURL;
} else {
axios.defaults.baseURL = config.baseURL;
}
/*** /***
* Intercept requests and insert Token if available * Intercept requests and insert Token if available
*/ */
axios.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
const session = store.state.session.currentSession; const session = store.state.sessions.currentSession;
if (session?.token) { if (session?.token) {
config.headers = { Authorization: 'Bearer ' + session.token }; config.headers = { Authorization: 'Bearer ' + session.token };
} }
@ -62,15 +42,39 @@ export default boot<Store<StateInterface>>(({ Vue, store, router }) => {
) { ) {
return router.push({ return router.push({
name: 'offline', name: 'offline',
query: { redirect: router.currentRoute.fullPath }, query: { redirect: router.currentRoute.value.fullPath },
}); });
} else if (e.response && e.response.status == 401) { } 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); 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);
};

View File

@ -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)
);
});

View File

@ -4,7 +4,7 @@ import DarkCircularProgress from 'components/loading/DarkCircularProgress.vue';
// "async" is optional; // "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file // 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({ Loading.setDefaults({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@ -1,11 +1,10 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { StateInterface } from 'src/store'; import { UserSessionState } from 'src/plugins/user/store';
import { RouteRecord } from 'vue-router'; import { RouteRecord } from 'vue-router';
import { Store } from 'vuex';
export default boot<Store<StateInterface>>(({ router, store }) => { export default boot<UserSessionState>(({ router, store }) => {
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const session = store.state.session.currentSession; const session = store.state.sessions.currentSession;
if (to.path == from.path) { if (to.path == from.path) {
return; return;
@ -29,7 +28,7 @@ export default boot<Store<StateInterface>>(({ router, store }) => {
if ((<{ permissions: FG.Permission[] }>record.meta).permissions) { if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
return (<{ permissions: FG.Permission[] }>record.meta).permissions.every( return (<{ permissions: FG.Permission[] }>record.meta).permissions.every(
(permission: string) => { (permission: string) => {
return store.state.user.currentPermissions.includes(permission); return store.state.users.currentPermissions.includes(permission);
} }
); );
} }
@ -41,7 +40,7 @@ export default boot<Store<StateInterface>>(({ router, store }) => {
next({ name: 'login', query: { redirect: to.fullPath } }); next({ name: 'login', query: { redirect: to.fullPath } });
} }
} else { } 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 // Called login while already logged in
void next({ name: 'dashboard' }); void next({ name: 'dashboard' });
} else { } else {

View File

@ -3,7 +3,7 @@ import { Notify } from 'quasar';
// "async" is optional; // "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/boot-files // more info on params: https://quasar.dev/quasar-cli/boot-files
export default boot((/* { app, router, Vue ... } */) => { export default boot(() => {
Notify.registerType('error', { Notify.registerType('error', {
color: 'negative', color: 'negative',
icon: 'mdi-alert-circle', icon: 'mdi-alert-circle',

View File

@ -1,11 +1,11 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { Store } from 'vuex'; import { Store } from 'vuex';
import { StateInterface } from 'src/store';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import routes from 'src/router/routes'; import routes from 'src/router/routes';
import { axios } from 'boot/axios'; import { axios } from 'boot/axios';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { Router, RouteRecordRaw } from 'vue-router'; import { Router, RouteRecordRaw } from 'vue-router';
import { UserSessionState } from 'src/plugins/user/store';
const config = { const config = {
// Do not change required Modules !! // Do not change required Modules !!
@ -154,7 +154,7 @@ function loadPlugin(
modules: string[], modules: string[],
backendpromise: Promise<Backend | null>, backendpromise: Promise<Backend | null>,
plugins: FG_Plugin.Plugin[], plugins: FG_Plugin.Plugin[],
store: Store<any>, store: Store<UserSessionState>,
router: Router router: Router
): FG_Plugin.LoadedPlugins { ): FG_Plugin.LoadedPlugins {
modules.forEach((requiredModule) => { modules.forEach((requiredModule) => {
@ -211,7 +211,7 @@ async function getBackend(): Promise<Backend | null> {
// "async" is optional; // "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file // more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
export default boot<Store<StateInterface>>(({ router, store, app }) => { export default boot<UserSessionState>(({ router, store, app }) => {
const plugins: FG_Plugin.Plugin[] = []; const plugins: FG_Plugin.Plugin[] = [];
const backendPromise = getBackend(); const backendPromise = getBackend();

View File

@ -0,0 +1,7 @@
import { SessionStateInterface } from './session';
import { UserStateInterface } from './user';
export interface UserSessionState {
sessions: SessionStateInterface;
users: UserStateInterface;
}

View File

@ -1,12 +1,12 @@
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'; import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { LoginData, LoginResponse } from 'src/plugins/user/models'; import { LoginData, LoginResponse } from 'src/plugins/user/models';
import { StateInterface } from 'src/store'; import { api } from 'src/boot/axios';
import { axios } from 'src/boot/axios';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { Router } from 'src/router';
import { LocalStorage } from 'quasar'; import { LocalStorage } from 'quasar';
import { UserSessionState } from '.';
import { useRouter } from 'vue-router';
export interface SessionInterface { export interface SessionStateInterface {
currentSession?: FG.Session; currentSession?: FG.Session;
sessions: FG.Session[]; sessions: FG.Session[];
loading: boolean; loading: boolean;
@ -22,13 +22,13 @@ function loadCurrentSession() {
return session; return session;
} }
const state: SessionInterface = { const state: SessionStateInterface = {
sessions: [], sessions: [],
currentSession: loadCurrentSession() || undefined, currentSession: loadCurrentSession() || undefined,
loading: false, loading: false,
}; };
const mutations: MutationTree<SessionInterface> = { const mutations: MutationTree<SessionStateInterface> = {
setCurrentSession(state, session: FG.Session) { setCurrentSession(state, session: FG.Session) {
LocalStorage.set('currentSession', session); LocalStorage.set('currentSession', session);
state.currentSession = session; state.currentSession = session;
@ -51,19 +51,22 @@ const mutations: MutationTree<SessionInterface> = {
}, },
}; };
const actions: ActionTree<SessionInterface, StateInterface> = { const actions: ActionTree<SessionStateInterface, UserSessionState> = {
/** Used to authenticate the user /** Used to authenticate the user
* Setting current Session, User and Permissions. * Setting current Session, User and Permissions.
* @param param0 Context * @param param0 Context
* @param data Credentitals * @param data Credentitals
*/ */
login({ commit, dispatch }, data: LoginData) { login({ commit }, data: LoginData) {
return axios return api
.post('/auth', data) .post('/auth', data)
.then(async (response: AxiosResponse<FG.Session>) => { .then((response: AxiosResponse<LoginResponse>) => {
response.data.expires = new Date(response.data.expires); response.data.session.expires = new Date(response.data.session.expires);
commit('setCurrentSession', response.data); commit('setCurrentSession', response.data.session);
await dispatch('user/getCurrentUser', undefined, { root: true }); commit('user/setCurrentUser', response.data.user, { root: true });
commit('user/setCurrentPermissions', response.data.permissions, {
root: true,
});
}) })
.catch((error: AxiosError) => { .catch((error: AxiosError) => {
return Promise.reject(error.response); return Promise.reject(error.response);
@ -74,8 +77,8 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
* Alias of deleteSession with current session as target * Alias of deleteSession with current session as target
*/ */
logout({ dispatch, rootState }) { logout({ dispatch, rootState }) {
if (rootState.session.currentSession) { if (rootState.sessions.currentSession) {
dispatch('deleteSession', rootState.session.currentSession.token).catch((error) => { dispatch('deleteSession', rootState.sessions.currentSession.token).catch((error) => {
console.log(error); console.log(error);
void dispatch('clearCurrent', false); void dispatch('clearCurrent', false);
}); });
@ -88,10 +91,10 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
*/ */
deleteSession({ commit, dispatch, rootState }, token: string) { deleteSession({ commit, dispatch, rootState }, token: string) {
commit('setLoading', true); commit('setLoading', true);
axios api
.delete(`/auth/${token}`) .delete(`/auth/${token}`)
.then(() => { .then(() => {
if (token === rootState.session.currentSession?.token) { if (token === rootState.sessions.currentSession?.token) {
void dispatch('clearCurrent', false); void dispatch('clearCurrent', false);
} else { } else {
dispatch('getSessions').catch((error) => { dispatch('getSessions').catch((error) => {
@ -110,15 +113,18 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
* Clear current session and logged in user * Clear current session and logged in user
*/ */
clearCurrent({ commit }, redirect = true) { clearCurrent({ commit }, redirect = true) {
void Router.push({ const router = useRouter();
void router
.push({
name: 'login', name: 'login',
query: redirect ? { redirect: Router.currentRoute.fullPath } : {}, query: redirect ? { redirect: router.currentRoute.value.fullPath } : {},
params: { logout: 'true' }, params: { logout: 'true' },
}).then(() => { })
.then(() => {
commit('clearCurrentSession'); commit('clearCurrentSession');
commit('user/clearCurrentUser', null, { root: true }); commit('user/clearCurrentUser', null, { root: true });
// ensure also volatile store gets cleared by refreshing the site // ensure also volatile store gets cleared by refreshing the site
Router.go(0); router.go(0);
}); });
}, },
/** /**
@ -126,7 +132,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
*/ */
getSessions({ commit, state }) { getSessions({ commit, state }) {
commit('setLoading', true); commit('setLoading', true);
axios api
.get('/auth') .get('/auth')
.then((response: AxiosResponse<FG.Session[]>) => { .then((response: AxiosResponse<FG.Session[]>) => {
response.data.forEach((session) => { response.data.forEach((session) => {
@ -149,7 +155,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
}, },
updateSession({ commit, state }, data: { lifetime: number; token: string }) { updateSession({ commit, state }, data: { lifetime: number; token: string }) {
commit('setLoading', true); commit('setLoading', true);
axios api
.put(`auth/${data.token}`, { value: data.lifetime }) .put(`auth/${data.token}`, { value: data.lifetime })
.then((response: AxiosResponse<FG.Session>) => { .then((response: AxiosResponse<FG.Session>) => {
response.data.expires = new Date(response.data.expires); response.data.expires = new Date(response.data.expires);
@ -164,16 +170,16 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
console.log('updateSession', data); console.log('updateSession', data);
}, },
requestPasswordReset({}, data) { requestPasswordReset({}, data) {
return axios.post('/auth/reset', data); return api.post('/auth/reset', data);
}, },
resetPassword({}, 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); return Promise.reject(error.response);
}); });
}, },
}; };
const getters: GetterTree<SessionInterface, StateInterface> = { const getters: GetterTree<SessionStateInterface, UserSessionState> = {
currentSession(state) { currentSession(state) {
return state.currentSession; return state.currentSession;
}, },
@ -185,7 +191,7 @@ const getters: GetterTree<SessionInterface, StateInterface> = {
}, },
}; };
const sessions: Module<SessionInterface, StateInterface> = { const sessions: Module<SessionStateInterface, UserSessionState> = {
namespaced: true, namespaced: true,
state, state,
mutations, mutations,

View File

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'; import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { StateInterface } from 'src/store'; import { api } from 'boot/axios';
import { axios } from 'boot/axios';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { SessionStorage } from 'quasar'; import { SessionStorage } from 'quasar';
import { CurrentUserResponse } from 'src/plugins/user/models'; import { CurrentUserResponse } from 'src/plugins/user/models';
import { UserSessionState } from '.';
export interface UserStateInterface { export interface UserStateInterface {
currentUser?: FG.User; currentUser?: FG.User;
@ -76,12 +76,12 @@ const mutations: MutationTree<UserStateInterface> = {
}, },
}; };
const actions: ActionTree<UserStateInterface, StateInterface> = { const actions: ActionTree<UserStateInterface, UserSessionState> = {
getCurrentUser({ commit, rootState }) { getCurrentUser({ commit, rootState }) {
if (rootState.session.currentSession) { if (rootState.sessions.currentSession) {
commit('setLoading'); commit('setLoading');
axios api
.get(`/users/${rootState.session.currentSession.userid}`) .get(`/users/${rootState.sessions.currentSession.userid}`)
.then((response: AxiosResponse<CurrentUserResponse>) => { .then((response: AxiosResponse<CurrentUserResponse>) => {
commit('setCurrentUser', response.data); commit('setCurrentUser', response.data);
commit('setCurrentPermissions', response.data.permissions); commit('setCurrentPermissions', response.data.permissions);
@ -99,7 +99,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
getUsers({ commit }) { getUsers({ commit }) {
commit('setLoading'); commit('setLoading');
axios api
.get('/users') .get('/users')
.then((response: AxiosResponse<FG.User[]>) => { .then((response: AxiosResponse<FG.User[]>) => {
response.data.forEach((user) => { response.data.forEach((user) => {
@ -117,7 +117,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
updateUser({ commit, state, dispatch }, data: FG.User) { updateUser({ commit, state, dispatch }, data: FG.User) {
commit('setLoading'); commit('setLoading');
axios api
.put(`/users/${data.userid}`, data) .put(`/users/${data.userid}`, data)
.then(() => { .then(() => {
if (state.currentUser && state.currentUser.userid === data.userid) if (state.currentUser && state.currentUser.userid === data.userid)
@ -136,7 +136,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
commit('setLoading'); commit('setLoading');
const formData = new FormData(); const formData = new FormData();
formData.append('file', payload.file); formData.append('file', payload.file);
return axios return api
.post(`/users/${payload.user.userid}/avatar`, formData, { .post(`/users/${payload.user.userid}/avatar`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
@ -152,7 +152,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
setUser({ commit, state, dispatch }, data: FG.User) { setUser({ commit, state, dispatch }, data: FG.User) {
commit('setLoading'); commit('setLoading');
axios api
.post('users', data) .post('users', data)
.then(() => { .then(() => {
if (state.currentUser && state.currentUser.userid === data.userid) if (state.currentUser && state.currentUser.userid === data.userid)
@ -170,7 +170,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
getRoles({ commit, state }, force = false) { getRoles({ commit, state }, force = false) {
if (!force && state.roles.length > 0) return; if (!force && state.roles.length > 0) return;
commit('setLoading'); commit('setLoading');
axios api
.get('/roles') .get('/roles')
.then((response: AxiosResponse<FG.Role[]>) => { .then((response: AxiosResponse<FG.Role[]>) => {
commit('setRoles', response.data); commit('setRoles', response.data);
@ -180,7 +180,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
updateRole({ commit }, data: FG.Role) { updateRole({ commit }, data: FG.Role) {
commit('setLoading'); commit('setLoading');
axios api
.put(`/roles/${data.id}`, data) .put(`/roles/${data.id}`, data)
.then(() => { .then(() => {
commit('updateRole', data); commit('updateRole', data);
@ -192,7 +192,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
newRole({ commit }, data: FG.Role) { newRole({ commit }, data: FG.Role) {
commit('setLoading'); commit('setLoading');
return axios return api
.post('/roles', data) .post('/roles', data)
.then((response: AxiosResponse<FG.Role>) => { .then((response: AxiosResponse<FG.Role>) => {
commit('addRole', response.data); commit('addRole', response.data);
@ -205,7 +205,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
deleteRole({ commit, state }, data: FG.Role) { deleteRole({ commit, state }, data: FG.Role) {
commit('setLoading'); commit('setLoading');
axios api
.delete(`/roles/${data.id}`) .delete(`/roles/${data.id}`)
.then(() => { .then(() => {
commit( commit(
@ -221,7 +221,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
getPermissions({ commit, state }, force = false) { getPermissions({ commit, state }, force = false) {
if (!force && state.permissions.length > 0) return; if (!force && state.permissions.length > 0) return;
commit('setLoading'); commit('setLoading');
axios api
.get('/roles/permissions') .get('/roles/permissions')
.then((response: AxiosResponse<FG.Role[]>) => { .then((response: AxiosResponse<FG.Role[]>) => {
commit('setPermissions', response.data); commit('setPermissions', response.data);
@ -232,7 +232,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const user = <FG.User | undefined>getters['getUser'](data.userid); const user = <FG.User | undefined>getters['getUser'](data.userid);
if (user === undefined || data.force === true) { if (user === undefined || data.force === true) {
return axios.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => { return api.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => {
commit('setUser', response.data); commit('setUser', response.data);
return response.data; return response.data;
}); });
@ -242,7 +242,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
}, },
}; };
const getters: GetterTree<UserStateInterface, StateInterface> = { const getters: GetterTree<UserStateInterface, UserSessionState> = {
getUser: (state) => (userid: string) => { getUser: (state) => (userid: string) => {
const user = state.users.filter((usr) => usr.userid === userid); const user = state.users.filter((usr) => usr.userid === userid);
return user.length > 0 ? user[0] : undefined; return user.length > 0 ? user[0] : undefined;
@ -264,7 +264,7 @@ const getters: GetterTree<UserStateInterface, StateInterface> = {
}, },
}; };
const userStore: Module<UserStateInterface, StateInterface> = { const userStore: Module<UserStateInterface, UserSessionState> = {
namespaced: true, namespaced: true,
actions, actions,
getters, getters,

9
src/store/store-flag.d.ts vendored Normal file
View File

@ -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;
}
}