release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
32 changed files with 515 additions and 835 deletions
Showing only changes of commit 4b198b6472 - Show all commits

View File

@ -14,6 +14,7 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"cordova": "^10.0.0", "cordova": "^10.0.0",
"core-js": "^3.7.0", "core-js": "^3.7.0",
"pinia": "^2.0.0-alpha.7",
"quasar": "^2.0.0-beta.9" "quasar": "^2.0.0-beta.9"
}, },
"prettier": { "prettier": {

View File

@ -30,7 +30,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://quasar.dev/quasar-cli/boot-files // https://quasar.dev/quasar-cli/boot-files
boot: ['axios', 'plugins', 'loading', 'login'], boot: ['axios', 'store', 'plugins', 'loading', 'login'],
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
css: ['app.scss'], css: ['app.scss'],

View File

@ -1,27 +1,21 @@
import config from 'src/config'; import config from 'src/config';
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { LocalStorage, Notify } from 'quasar'; import { LocalStorage, Notify } from 'quasar';
import axios, { AxiosError, AxiosInstance } from 'axios'; import axios, { AxiosError } from 'axios';
import { UserSessionState } from 'src/plugins/user/store'; import { useMainStore } from 'src/store';
declare module '@vue/runtime-core' { const api = axios.create();
interface ComponentCustomProperties {
$axios: AxiosInstance;
}
}
const api = axios.create({ export default boot(({ store, router }) => {
baseURL: <string | undefined>LocalStorage.getItem('baseURL') || config.baseURL, api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
});
export default boot<UserSessionState>(({ app, store, router }) => {
/*** /***
* Intercept requests and insert Token if available * Intercept requests and insert Token if available
*/ */
api.interceptors.request.use((config) => { api.interceptors.request.use((config) => {
const session = store.state.sessions.currentSession; const store = useMainStore();
if (session?.token) { if (store.session?.token) {
config.headers = { Authorization: 'Bearer ' + session.token }; config.headers = { Authorization: 'Bearer ' + store.session.token };
} }
return config; return config;
}); });
@ -31,7 +25,7 @@ export default boot<UserSessionState>(({ app, store, router }) => {
* - filter 401 --> logout * - filter 401 --> logout
* - filter timeout or 502-504 --> backendOffline * - filter timeout or 502-504 --> backendOffline
*/ */
axios.interceptors.response.use( api.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
if (error) { if (error) {
@ -56,22 +50,13 @@ export default boot<UserSessionState>(({ app, store, router }) => {
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, api }; export { api };
export const setBaseUrl = (url: string) => { export const setBaseURL = (url: string) => {
LocalStorage.set('baseURL', url); LocalStorage.set('baseURL', url);
axios.defaults.baseURL = url; api.defaults.baseURL = url;
Notify.create({ Notify.create({
message: 'Serveraddresse gespeichert', message: 'Serveraddresse gespeichert',
position: 'bottom', position: 'bottom',

View File

@ -1,10 +1,12 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { UserSessionState } from 'src/plugins/user/store'; import { useMainStore } from 'src/store';
import { hasPermissions } from 'src/utils/permission';
import { RouteRecord } from 'vue-router'; import { RouteRecord } from 'vue-router';
export default boot<UserSessionState>(({ router, store }) => { export default boot(({ router }) => {
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const session = store.state.sessions.currentSession; console.log(`from ${from.fullPath} to ${to.fullPath}`);
const store = useMainStore();
if (to.path == from.path) { if (to.path == from.path) {
return; return;
@ -13,10 +15,9 @@ export default boot<UserSessionState>(({ router, store }) => {
if (to.path.startsWith('/main')) { if (to.path.startsWith('/main')) {
// Secured area (LOGIN REQUIRED) // Secured area (LOGIN REQUIRED)
// Check login is ok // Check login is ok
if (!session || session.expires <= new Date()) { if (!store.session || store.session.expires <= new Date()) {
store.dispatch('sessions/logout').catch((error) => { console.log('Nope, logout');
console.warn(error); void store.logout();
});
return; return;
} }
@ -24,27 +25,28 @@ export default boot<UserSessionState>(({ router, store }) => {
if ( if (
to.matched.every((record: RouteRecord) => { to.matched.every((record: RouteRecord) => {
if (!('meta' in record) || !('permissions' in record.meta)) return true; if (!('meta' in record) || !('permissions' in record.meta)) return true;
if (record.meta) { if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
if ((<{ permissions: FG.Permission[] }>record.meta).permissions) { console.log(record.meta);
return (<{ permissions: FG.Permission[] }>record.meta).permissions.every( const h = hasPermissions((<{ permissions: FG.Permission[] }>record.meta).permissions);
(permission: string) => { console.log(h);
return store.state.users.currentPermissions.includes(permission); return h;
}
);
}
} }
}) })
) { ) {
console.log('ok next');
next(); next();
} else { } else {
console.log('Back loggin');
next({ name: 'login', query: { redirect: to.fullPath } }); next({ name: 'login', query: { redirect: to.fullPath } });
} }
} else { } else {
if (to.name == 'login' && store.state.users.currentUser && !to.params['logout']) { if (to.name == 'login' && store.user && !to.params['logout']) {
// Called login while already logged in // Called login while already logged in
console.log('Ok next');
void next({ name: 'dashboard' }); void next({ name: 'dashboard' });
} else { } else {
// We are on the non secured area // We are on the non secured area
console.log('Ok non sec');
next(); next();
} }
} }

View File

@ -1,14 +0,0 @@
import { boot } from 'quasar/wrappers';
import { Notify } from 'quasar';
// "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/boot-files
export default boot(() => {
Notify.registerType('error', {
color: 'negative',
icon: 'mdi-alert-circle',
progress: true,
position: 'bottom',
actions: [{ icon: 'mdi-close', color: 'white' }],
});
});

View File

@ -5,7 +5,6 @@ import routes from 'src/router/routes';
import { api } from 'boot/axios'; import { api } 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: { [key: string]: Array<string> } = { const config: { [key: string]: Array<string> } = {
// Do not change required Modules !! // Do not change required Modules !!
@ -160,7 +159,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<UserSessionState>, store: Store<unknown>,
router: Router router: Router
): FG_Plugin.Flaschengeist { ): FG_Plugin.Flaschengeist {
modules.forEach((requiredModule) => { modules.forEach((requiredModule) => {
@ -218,7 +217,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<UserSessionState>(({ router, store, app }) => { export default boot(({ router, app, store }) => {
const plugins: FG_Plugin.Plugin[] = []; const plugins: FG_Plugin.Plugin[] = [];
const backendPromise = getBackend(); const backendPromise = getBackend();

10
src/boot/store.ts Normal file
View File

@ -0,0 +1,10 @@
import { createPinia } from 'pinia';
import { boot } from 'quasar/wrappers';
import { useMainStore } from 'src/store';
export default boot(({ app }) => {
app.use(createPinia());
const store = useMainStore();
void store.init();
});

View File

@ -5,7 +5,7 @@
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label>{{ realTitle }}</q-item-label> <q-item-label>{{ title }}</q-item-label>
<!--<q-item-label caption> <!--<q-item-label caption>
{{ caption }} {{ caption }}
</q-item-label>--> </q-item-label>-->
@ -14,7 +14,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import { hasPermissions } from 'src/utils/permission'; import { hasPermissions } from 'src/utils/permission';
@ -44,22 +43,8 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
let title = computed<string>(() => {
if (props.title.includes('loadFromStore')) {
const store = useStore();
const startIndex = props.title.indexOf('(') + 1;
const endIndex = props.title.indexOf(')');
const substring = props.title.substring(startIndex, endIndex).replace(/"/g, '');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return <string>store.getters[substring];
}
return props.title;
});
const isGranted = computed(() => hasPermissions(props.permissions)); const isGranted = computed(() => hasPermissions(props.permissions));
return { isGranted };
return { realTitle: title, isGranted };
}, },
}); });
</script> </script>

View File

@ -13,10 +13,10 @@ declare namespace FG {
firstname: string; firstname: string;
lastname: string; lastname: string;
mail: string; mail: string;
birthday?: any; birthday?: Date;
roles: Array<string>; roles: Array<string>;
permissions?: any; permissions?: Array<Permission>;
avatar_url?: any; avatar_url?: string;
} }
type Permission = string; type Permission = string;
interface Role { interface Role {
@ -29,51 +29,16 @@ declare namespace FG {
time: Date; time: Date;
amount: number; amount: number;
reversal_id: number; reversal_id: number;
sender_id?: any; sender_id?: string;
receiver_id?: any; receiver_id?: string;
author_id?: any; author_id?: string;
original_id?: any; original_id?: number;
}
interface Drink {
id: number;
name: string;
volume: number;
cost_price: number;
discount: number;
extra_charge?: any;
prices: Array<DrinkPrice>;
ingredients: Array<Ingredient>;
tags: Array<any>;
}
interface DrinkPrice {
id: number;
volume: number;
price: number;
no_auto: boolean;
public: boolean;
description?: any;
round_step: number;
}
interface DrinkType {
id: number;
name: string;
}
interface Ingredient {
id: number;
volume: number;
drink_parent_id: number;
drink_ingredient_id: number;
drink_ingredient?: any;
}
interface Tag {
id: number;
name: string;
} }
interface Event { interface Event {
id: number; id: number;
start: Date; start: Date;
end: Date; end: Date;
description?: any; description?: string;
type: EventType; type: EventType;
jobs: Array<Job>; jobs: Array<Job>;
} }
@ -84,7 +49,7 @@ declare namespace FG {
interface Job { interface Job {
id: number; id: number;
start: Date; start: Date;
end?: any; end?: Date;
comment: string; comment: string;
type: JobType; type: JobType;
services: Array<Service>; services: Array<Service>;

View File

@ -48,7 +48,7 @@
<essential-link <essential-link
v-for="(link, index) in flaschengeist.mainLinks" v-for="(link, index) in flaschengeist.mainLinks"
:key="'plugin' + index" :key="'plugin' + index"
:title="link.title" :title="typeof link.title === 'object' ? link.title.value : link.title"
:link="link.link" :link="link.link"
:icon="link.icon" :icon="link.icon"
:permissions="link.permissions" :permissions="link.permissions"
@ -101,11 +101,11 @@
<script lang="ts"> <script lang="ts">
import EssentialLink from 'components/navigation/EssentialLink.vue'; import EssentialLink from 'components/navigation/EssentialLink.vue';
import ShortCutLink from 'components/navigation/ShortCutLink.vue'; import ShortCutLink from 'components/navigation/ShortCutLink.vue';
import { Screen, Loading } from 'quasar'; import { Screen } from 'quasar';
import { defineComponent, ref, inject, computed } from 'vue'; import { defineComponent, ref, inject, computed } from 'vue';
import { useMainStore } from 'src/store';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
const links = [ const links = [
{ {
@ -136,7 +136,7 @@ export default defineComponent({
components: { EssentialLink, ShortCutLink }, components: { EssentialLink, ShortCutLink },
setup() { setup() {
const route = useRoute(); const route = useRoute();
const store = useStore(); const mainStore = useMainStore();
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist'); const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
const leftDrawer = ref(false); const leftDrawer = ref(false);
@ -171,10 +171,7 @@ export default defineComponent({
}); });
function logout() { function logout() {
Loading.show({ message: 'Session wird abgemeldet' }); void mainStore.logout();
store.dispatch('sessions/logout').finally(() => {
Loading.hide();
});
} }
return { return {

View File

@ -54,19 +54,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex'; import { useMainStore } from 'src/store';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Loading, Notify } from 'quasar'; import { Loading, Notify } from 'quasar';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { setBaseUrl, api } from 'boot/axios'; import { setBaseURL, api } from 'boot/axios';
import { UserSessionState } from 'src/plugins/user/store'; import { useUserStore } from 'src/plugins/user/store';
export default defineComponent({ export default defineComponent({
// name: 'PageName' name: 'Login',
setup() { setup() {
const store = useStore<UserSessionState>(); const mainStore = useMainStore();
const router = useRouter();
const mainRoute = { name: 'dashboard' }; const mainRoute = { name: 'dashboard' };
const router = useRouter();
/* Stuff for the real login page */ /* Stuff for the real login page */
const userid = ref(''); const userid = ref('');
@ -81,42 +81,35 @@ export default defineComponent({
function changeUrl() { function changeUrl() {
if (server.value) { if (server.value) {
setBaseUrl(server.value); setBaseURL(server.value);
} }
} }
function doLogin() { async function doLogin() {
Loading.show({ Loading.show({ message: 'Du wirst angemeldet' });
message: 'Du wirst angemeldet', const status = await mainStore.login(userid.value, password.value);
});
store if (status === true) {
.dispatch('sessions/login', { mainStore.user = (await useUserStore().getUser(userid.value)) || undefined;
userid: userid.value, const x = router.currentRoute.value.query['redirect'];
password: password.value, void router.push(typeof x === 'string' ? { path: x } : mainRoute);
}) } else {
.then(() => { password.value = '';
const x = router.currentRoute.value.query['redirect']; if (status === 401) {
void router.push(typeof x === 'string' ? { path: x } : mainRoute); Notify.create({
}) group: false,
.catch((error: { status: number } | undefined) => { type: 'negative',
if (error && error.status === 401) { message: 'Benutzername oder Passwort sind falsch.',
password.value = ''; timeout: 10000,
Notify.create({ progress: true,
group: false, actions: [{ icon: 'mdi-close', color: 'white' }],
type: 'negative', });
message: 'Benutzername oder Passwort sind falsch.', }
timeout: 10000, }
progress: true, Loading.hide();
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
})
.finally(() => {
Loading.hide();
});
} }
function doReset() { async function doReset() {
if (userid.value == '') { if (userid.value == '') {
Notify.create({ Notify.create({
group: false, group: false,
@ -128,23 +121,20 @@ export default defineComponent({
}); });
return; return;
} }
void store
.dispatch('sessions/requestPasswordReset', { if (await mainStore.requestReset(userid.value)) {
userid: userid.value, userid.value = '';
}) password.value = '';
.then(() => { Notify.create({
userid.value = ''; group: false,
password.value = ''; type: 'ongoing',
Notify.create({ message:
group: false, 'Sollte der Benutzername korrekt und vorhanden sein, erhälst du jetzt eine E-Mail.',
type: 'ongoing', timeout: 10000,
message: progress: true,
'Sollte der Benutzername korrekt und vorhanden sein, erhälst du jetzt eine E-Mail.', actions: [{ icon: 'mdi-close', color: 'white' }],
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}); });
}
} }
return { return {

View File

@ -47,8 +47,6 @@ export default defineComponent({
reload.value -= 1; reload.value -= 1;
if (reload.value === 0) { if (reload.value === 0) {
const path = router.currentRoute.value.query.redirect; const path = router.currentRoute.value.query.redirect;
console.log('Offline: ');
console.log(path);
void router.replace(path ? { path: <string>path } : { name: 'login' }); void router.replace(path ? { path: <string>path } : { name: 'login' });
} }
}, 1000); }, 1000);

View File

@ -34,17 +34,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex';
import { AxiosResponse } from 'axios';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Loading, Notify } from 'quasar'; import { Loading, Notify } from 'quasar';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { UserSessionState } from 'src/plugins/user/store'; import { useMainStore } from 'src/store';
export default defineComponent({ export default defineComponent({
// name: 'PageName' // name: 'PageName'
setup() { setup() {
const store = useStore<UserSessionState>(); const mainStore = useMainStore();
const router = useRouter(); const router = useRouter();
const password = ref(''); const password = ref('');
const password2 = ref(''); const password2 = ref('');
@ -68,16 +66,27 @@ export default defineComponent({
return; return;
} }
const token = router.currentRoute.value.query.token;
if (token === null)
if (password.value !== password2.value) {
Notify.create({
group: false,
type: 'negative',
message: 'Der Link wurde nicht richtig geöffnet, Token nicht gefunden.',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
return;
}
Loading.show({ Loading.show({
message: 'Das Passwort wird zurückgesetzt', message: 'Das Passwort wird zurückgesetzt',
}); });
store mainStore
.dispatch('sessions/resetPassword', { .resetPassword(<string>token, password.value)
password: password.value, .catch((status) => {
token: router.currentRoute.value.query.token, if (status == 403) {
})
.catch((error: AxiosResponse) => {
if (error.status == 403) {
Notify.create({ Notify.create({
group: false, group: false,
type: 'negative', type: 'negative',

4
src/plugins.d.ts vendored
View File

@ -1,6 +1,6 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Module } from 'vuex'; import { Module } from 'vuex';
import { Component } from 'vue'; import { Component, ComputedRef } from 'vue';
declare namespace FG_Plugin { declare namespace FG_Plugin {
interface ShortCutLink { interface ShortCutLink {
@ -10,7 +10,7 @@ declare namespace FG_Plugin {
} }
interface PluginRouteConfig { interface PluginRouteConfig {
title: string; title: string | ComputedRef<string>;
icon: string; icon: string;
route: RouteRecordRaw; route: RouteRecordRaw;
shortcut?: boolean; shortcut?: boolean;

View File

@ -1,5 +1,5 @@
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex'; import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { StateInterface } from 'src/store'; import { StateInterface, useMainStore } from 'src/store';
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
@ -90,27 +90,28 @@ const mutations: MutationTree<BalanceInterface> = {
const actions: ActionTree<BalanceInterface, StateInterface> = { const actions: ActionTree<BalanceInterface, StateInterface> = {
//const actions: ActionTree<BalanceInterface, any> = { //const actions: ActionTree<BalanceInterface, any> = {
addShortcut({ commit, state, rootState }, shortcut) { addShortcut({ commit, state }, shortcut) {
const sc = [...state.shortcuts, shortcut]; const sc = [...state.shortcuts, shortcut];
sc.sort(); sc.sort();
const mainStore = useMainStore();
const user = <FG.User>rootState.users.currentUser; const user = mainStore.currentUser;
return api.put(`/users/${user.userid}/balance/shortcuts`, sc).then(() => { return api.put(`/users/${user.userid}/balance/shortcuts`, sc).then(() => {
commit('setShortcuts', sc); commit('setShortcuts', sc);
}); });
}, },
removeShortcut({ commit, state, rootState }, shortcut) { removeShortcut({ commit, state }, shortcut) {
const sc = state.shortcuts.filter((value: number) => value != shortcut); const sc = state.shortcuts.filter((value: number) => value != shortcut);
const mainStore = useMainStore();
const user = <FG.User>rootState.users.currentUser; const user = mainStore.currentUser;
return api.put(`/users/${user.userid}/balance/shortcuts`, sc).then(() => { return api.put(`/users/${user.userid}/balance/shortcuts`, sc).then(() => {
commit('setShortcuts', sc); commit('setShortcuts', sc);
}); });
}, },
getShortcuts({ commit, state, rootState }, force = false) { getShortcuts({ commit, state }, force = false) {
if (force || state.shortcuts.length == 0) { if (force || state.shortcuts.length == 0) {
const mainStore = useMainStore();
commit('setLoading'); commit('setLoading');
const user = <FG.User>rootState.users.currentUser; const user = mainStore.currentUser;
return api return api
.get(`/users/${user.userid}/balance/shortcuts`) .get(`/users/${user.userid}/balance/shortcuts`)
.then(({ data }: AxiosResponse<BalanceResponse>) => { .then(({ data }: AxiosResponse<BalanceResponse>) => {
@ -120,9 +121,10 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
.finally(() => commit('setLoading', false)); .finally(() => commit('setLoading', false));
} }
}, },
getBalance({ commit, rootState }, user: FG.User | undefined = undefined) { getBalance({ commit }, user: FG.User | undefined = undefined) {
commit('setLoading'); commit('setLoading');
if (!user) user = <FG.User>rootState.users.currentUser; const mainStore = useMainStore();
if (!user) user = mainStore.currentUser;
return api return api
.get(`/users/${user.userid}/balance`) .get(`/users/${user.userid}/balance`)
.then(({ data }: AxiosResponse<BalanceResponse>) => { .then(({ data }: AxiosResponse<BalanceResponse>) => {
@ -137,7 +139,7 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
}); });
}, },
getTransactions( getTransactions(
{ commit, rootState }, { commit },
payload: { payload: {
userid?: string; userid?: string;
filter?: { filter?: {
@ -151,7 +153,8 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
} }
) { ) {
commit('setLoading'); commit('setLoading');
if (!payload.userid) payload.userid = (<FG.User>rootState.users.currentUser).userid; const mainStore = useMainStore();
if (!payload.userid) payload.userid = mainStore.currentUser.userid;
if (!payload.filter) payload.filter = { limit: 10 }; if (!payload.filter) payload.filter = { limit: 10 };
return api return api
.get(`/users/${payload.userid}/balance/transactions`, { params: payload.filter || {} }) .get(`/users/${payload.userid}/balance/transactions`, { params: payload.filter || {} })
@ -162,11 +165,12 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
}) })
.finally(() => commit('setLoading', false)); .finally(() => commit('setLoading', false));
}, },
getLimit({ rootState, commit }) { getLimit({ commit }) {
const mainStore = useMainStore();
commit('setLoading'); commit('setLoading');
api api
/* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */ /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
.get(`/users/${rootState.users.currentUser?.userid}/balance/limit`) .get(`/users/${mainStore.currentUser.userid}/balance/limit`)
.then(({ data }) => { .then(({ data }) => {
console.log(data); console.log(data);
}) })
@ -184,19 +188,17 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
dispatch('getBalance').catch((err) => console.warn(err)); dispatch('getBalance').catch((err) => console.warn(err));
}); });
}, },
changeBalance( changeBalance({ dispatch, commit }, data: { amount: number; user: string; sender?: string }) {
{ dispatch, commit, rootState },
data: { amount: number; user: string; sender?: string }
) {
commit('setLoading'); commit('setLoading');
return api return api
.put(`/users/${data.user}/balance`, data) .put(`/users/${data.user}/balance`, data)
.then((response: AxiosResponse<FG.Transaction>) => { .then((response: AxiosResponse<FG.Transaction>) => {
const mainStore = useMainStore();
const transaction = response.data; const transaction = response.data;
fixTransaction(transaction); fixTransaction(transaction);
if ( if (
data.user == rootState.users.currentUser?.userid || data.user == mainStore.currentUser.userid ||
data.sender === rootState.users.currentUser?.userid data.sender === mainStore.currentUser.userid
) )
commit('addTransaction', transaction); commit('addTransaction', transaction);
commit(state.balances.has(data.user) ? 'changeBalance' : 'setBalance', { commit(state.balances.has(data.user) ? 'changeBalance' : 'setBalance', {

View File

@ -12,14 +12,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import MainUserSettings from 'src/plugins/user/components/settings/MainUserSettings.vue'; import MainUserSettings from 'src/plugins/user/components/settings/MainUserSettings.vue';
import { useStore } from 'vuex'; import { useUserStore } from '../store';
import { UserSessionState } from '../store';
export default defineComponent({ export default defineComponent({
name: 'NewUser', name: 'NewUser',
components: { MainUserSettings }, components: { MainUserSettings },
setup() { setup() {
const store = useStore<UserSessionState>(); const userStore = useUserStore();
const user = ref<FG.User>({ const user = ref<FG.User>({
userid: '', userid: '',
display_name: '', display_name: '',
@ -28,10 +27,9 @@ export default defineComponent({
mail: '', mail: '',
roles: [], roles: [],
}); });
function setUser(value: FG.User) {
store.dispatch('users/setUser', value).catch((error) => { async function setUser(value: FG.User) {
console.warn(error); await userStore.createUser(value);
});
} }
return { user, setUser }; return { user, setUser };
}, },

View File

@ -11,23 +11,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { UserSessionState } from '../store';
import UserSelector from '../components/UserSelector.vue'; import UserSelector from '../components/UserSelector.vue';
import MainUserSettings from '../components/settings/MainUserSettings.vue'; import MainUserSettings from '../components/settings/MainUserSettings.vue';
import { useMainStore } from 'src/store';
import { useUserStore } from '../store';
export default defineComponent({ export default defineComponent({
name: 'UpdateUser', name: 'UpdateUser',
components: { UserSelector, MainUserSettings }, components: { UserSelector, MainUserSettings },
setup() { setup() {
const store = useStore<UserSessionState>(); const mainStore = useMainStore();
const user = ref(<FG.User>store.state.users.currentUser); const userStore = useUserStore();
const user = ref(mainStore.currentUser);
function updateUser(value: FG.User) { async function updateUser(value: FG.User) {
store.dispatch('users/updateUser', value).catch((error) => { await userStore.updateUser(value);
console.warn(error);
});
} }
return { return {

View File

@ -12,8 +12,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType, onBeforeMount } from 'vue'; import { computed, defineComponent, PropType, onBeforeMount } from 'vue';
import { useStore } from 'vuex'; import { useUserStore } from '../store';
import { UserSessionState } from '../store';
export default defineComponent({ export default defineComponent({
name: 'UserSelector', name: 'UserSelector',
@ -23,15 +22,13 @@ export default defineComponent({
}, },
emits: { 'update:modelValue': (user: FG.User) => !!user }, emits: { 'update:modelValue': (user: FG.User) => !!user },
setup(props, { emit }) { setup(props, { emit }) {
const store = useStore<UserSessionState>(); const userStore = useUserStore();
onBeforeMount(() => { onBeforeMount(() => {
store.dispatch('users/getUsers').catch((error) => { void userStore.getUsers(false);
console.error(error);
});
}); });
const users = computed(() => store.state.users.users); const users = computed(() => userStore.users);
const selected = computed({ const selected = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined), set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined),

View File

@ -26,19 +26,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useMainStore } from 'src/store';
import { computed, defineComponent, onMounted, ref } from 'vue'; import { computed, defineComponent, onMounted, ref } from 'vue';
import { useStore } from 'vuex'; import { useUserStore } from '../store';
import { UserSessionState } from '../store';
export default defineComponent({ export default defineComponent({
name: 'Greeting', name: 'Greeting',
setup() { setup() {
const store = useStore<UserSessionState>(); const mainStore = useMainStore();
onMounted(() => store.dispatch('users/getUsers', false)); const userStore = useUserStore();
const name = ref(store.state.users.currentUser?.display_name); // Ensure users are loaded,so we can query birthdays
onMounted(() => userStore.getUsers(false));
const avatarLink = ref(store.state.users.currentUser?.avatar_url); const name = ref(mainStore.currentUser.display_name);
const avatarLink = ref(mainStore.currentUser.avatar_url);
function userHasBirthday(user: FG.User) { function userHasBirthday(user: FG.User) {
const today = new Date(); const today = new Date();
@ -50,13 +52,13 @@ export default defineComponent({
} }
const hasBirthday = computed(() => { const hasBirthday = computed(() => {
return userHasBirthday(<FG.User>store.state.users.currentUser); return userHasBirthday(mainStore.currentUser);
}); });
const birthday = computed(() => const birthday = computed(() =>
store.state.users.users userStore.users
.filter(userHasBirthday) .filter(userHasBirthday)
.filter((user) => user.userid !== store.state.users.currentUser?.userid) .filter((user) => user.userid !== mainStore.currentUser.userid)
); );
return { avatarLink, name, hasBirthday, birthday }; return { avatarLink, name, hasBirthday, birthday };

View File

@ -107,12 +107,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex';
import { Notify } from 'quasar'; import { Notify } from 'quasar';
import { UserSessionState } from '../../store';
import { hasPermission } from 'src/utils/permission'; import { hasPermission } from 'src/utils/permission';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue'; import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { defineComponent, computed, ref, onBeforeMount, PropType } from 'vue'; import { defineComponent, computed, ref, onBeforeMount, PropType } from 'vue';
import { useUserStore } from '../../store';
import { useMainStore } from 'src/store';
export default defineComponent({ export default defineComponent({
name: 'MainUserSettings', name: 'MainUserSettings',
@ -128,22 +128,20 @@ export default defineComponent({
'update:user': (payload: FG.User) => !!payload, 'update:user': (payload: FG.User) => !!payload,
}, },
setup(props, { emit }) { setup(props, { emit }) {
const store = useStore<UserSessionState>(); const userStore = useUserStore();
const mainStore = useMainStore();
const user_model = ref(props.user); const user_model = ref(props.user);
onBeforeMount(() => { onBeforeMount(() => {
store.dispatch('users/getRoles', false).catch((error) => { void userStore.getRoles(false);
console.warn(error);
});
}); });
const isCurrentUser = computed( const isCurrentUser = computed(() => user_model.value.userid === mainStore.currentUser.userid);
() => user_model.value.userid === store.state.users.currentUser?.userid
);
const canSetRoles = computed(() => hasPermission('users_set_roles')); const canSetRoles = computed(() => hasPermission('users_set_roles'));
const avatar = ref([]); const avatar = ref([] as string[]);
function onAvatarRejected() { function onAvatarRejected() {
Notify.create({ Notify.create({
group: false, group: false,
@ -156,7 +154,7 @@ export default defineComponent({
avatar.value = []; avatar.value = [];
} }
const allRoles = computed(() => store.state.users.roles.map((role) => role.name)); const allRoles = computed(() => userStore.roles.map((role) => role.name));
const password = ref(''); const password = ref('');
const new_password = ref(''); const new_password = ref('');
const new_password2 = ref(''); const new_password2 = ref('');
@ -176,16 +174,11 @@ export default defineComponent({
emit('update:user', changed); emit('update:user', changed);
if (avatar.value && (avatar.value.length > 0 || avatar.value instanceof File)) if (avatar.value && (avatar.value.length > 0 || avatar.value instanceof File))
store userStore.uploadAvatar(changed, avatar.value[0]).catch((response: Response) => {
.dispatch('users/uploadAvatar', { if (response && response.status == 400) {
user: changed, onAvatarRejected();
file: avatar.value, }
}) });
.catch((response: Response) => {
if (response && response.status == 400) {
onAvatarRejected();
}
});
reset(); reset();
} }
@ -212,7 +205,7 @@ export default defineComponent({
function isUseridUsed(val: string) { function isUseridUsed(val: string) {
return ( return (
!store.state.users.users.find((user: FG.User) => { !userStore.users.find((user: FG.User) => {
return user.userid == val; return user.userid == val;
}) || }) ||
!props.newUser || !props.newUser ||

View File

@ -45,28 +45,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex';
import { UserSessionState } from '../../store';
import { computed, defineComponent, ref, onBeforeMount } from 'vue'; import { computed, defineComponent, ref, onBeforeMount } from 'vue';
import { useUserStore } from '../../store';
export default defineComponent({ export default defineComponent({
name: 'RoleSettings', name: 'RoleSettings',
setup() { setup() {
const store = useStore<UserSessionState>(); const userStore = useUserStore();
onBeforeMount(() => { onBeforeMount(() => {
store.dispatch('users/getRoles').catch((error) => { void userStore.getRoles();
console.warn(error); void userStore.getPermissions();
});
store.dispatch('users/getPermissions').catch((error) => {
console.warn(error);
});
}); });
const role = ref<FG.Role | null>(null); const role = ref<FG.Role | null>(null);
const roles = computed(() => store.state.users.roles); const roles = computed(() => userStore.roles);
const permissions = computed(() => const permissions = computed(() =>
store.state.users.permissions.map((perm) => { userStore.permissions.map((perm) => {
return { return {
value: perm, value: perm,
label: perm, label: perm,
@ -103,14 +98,14 @@ export default defineComponent({
function save() { function save() {
if (role.value) { if (role.value) {
if (role.value.id === -1) if (role.value.id === -1)
void store.dispatch('users/newRole', role.value).then((createdRole: FG.Role) => { void userStore.newRole(role.value).then((createdRole: FG.Role) => {
console.log(createdRole); console.log(createdRole);
role.value = createdRole; role.value = createdRole;
}); });
else { else {
if (newRoleName.value !== '') role.value.name = newRoleName.value; if (newRoleName.value !== '') role.value.name = newRoleName.value;
console.log(role.value); console.log(role.value);
void store.dispatch('users/updateRole', role.value); void userStore.updateRole(role.value);
} }
} }
} }
@ -129,10 +124,7 @@ export default defineComponent({
if (role.value.id === -1) { if (role.value.id === -1) {
role.value = null; role.value = null;
} else { } else {
store void userStore.deleteRole(role.value).then(() => (role.value = null));
.dispatch('users/deleteRole', role.value)
.then(() => (role.value = null))
.catch((error) => console.warn(error));
} }
} }
} }

View File

@ -46,21 +46,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useStore } from 'vuex'; import { defineComponent, ref, computed, PropType } from 'vue';
import { defineComponent, ref, computed } from 'vue';
import { UserSessionState } from '../../store';
import { formatDateTime } from 'src/utils/datetime'; import { formatDateTime } from 'src/utils/datetime';
import { useMainStore } from 'src/store';
import { useSessionStore } from '../../store';
export default defineComponent({ export default defineComponent({
name: 'Sessions', name: 'Sessions',
props: { props: {
session: { session: {
required: true, required: true,
type: Object, type: Object as PropType<FG.Session>,
}, },
}, },
setup(props) { setup(props) {
const store = useStore<UserSessionState>(); const sessionStore = useSessionStore();
const mainStore = useMainStore();
const dateTime = (date: Date) => formatDateTime(date, true); const dateTime = (date: Date) => formatDateTime(date, true);
@ -91,13 +92,11 @@ export default defineComponent({
: 'mdi-help'; : 'mdi-help';
} }
function deleteSession(token: string) { async function deleteSession(token: string) {
store.dispatch('sessions/deleteSession', token).catch((error) => { await sessionStore.deleteSession(token);
console.warn(error);
});
} }
function isThisSession(token: string) { function isThisSession(token: string) {
return store.state.sessions.currentSession?.token === token; return mainStore.session?.token === token;
} }
const isEdit = ref(false); const isEdit = ref(false);
@ -132,21 +131,14 @@ export default defineComponent({
}); });
function edit(value: boolean) { function edit(value: boolean) {
lifetime.value = (<FG.Session>props.session).lifetime; lifetime.value = props.session.lifetime;
isEdit.value = value; isEdit.value = value;
} }
function save() { async function save() {
console.log(lifetime.value); console.log(lifetime.value);
isEdit.value = false; isEdit.value = false;
void store await sessionStore.updateSession(lifetime.value, props.session.token);
.dispatch('sessions/updateSession', {
lifetime: lifetime.value,
token: (<FG.Session>props.session).token,
})
.catch((error) => {
console.log(error);
});
} }
return { return {

View File

@ -14,36 +14,29 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from 'vue'; import { defineComponent, onBeforeMount, ref } from 'vue';
import Sessions from '../components/settings/Sessions.vue'; import Sessions from '../components/settings/Sessions.vue';
import MainUserSettings from '../components/settings/MainUserSettings.vue'; import MainUserSettings from '../components/settings/MainUserSettings.vue';
import { useStore } from 'vuex'; import { useMainStore } from 'src/store';
import setLoadingBar from 'src/utils/loading'; import { useSessionStore } from '../store';
import { UserSessionState } from '../store'; import { useUserStore } from '../store';
export default defineComponent({ export default defineComponent({
// name: 'PageName' // name: 'PageName'
components: { Sessions, MainUserSettings }, components: { Sessions, MainUserSettings },
setup() { setup() {
const store = useStore<UserSessionState>(); const mainStore = useMainStore();
const sessionStore = useSessionStore();
const userStore = useUserStore();
onBeforeMount(() => { onBeforeMount(() => sessionStore.getSessions().then((s) => (sessions.value = s)));
store.dispatch('sessions/getSessions').catch((error) => { const currentUser = ref(mainStore.currentUser);
console.warn(error); const sessions = ref([] as FG.Session[]);
});
});
const currentUser = ref(<FG.User>store.state.users.currentUser); async function updateUser(value: FG.User) {
const sessions = computed(() => store.state.sessions.sessions); await userStore.updateUser(value);
const loading = computed(() => store.state.sessions.loading || store.state.users.loading > 0);
function updateUser(value: FG.User) {
store.dispatch('users/updateUser', value).catch((error) => {
console.warn(error);
});
} }
setLoadingBar(loading);
return { return {
currentUser, currentUser,
sessions, sessions,

View File

@ -1,10 +1,6 @@
import { Module } from 'vuex';
import routes from './routes'; import routes from './routes';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import { defineAsyncComponent } from 'vue'; import { defineAsyncComponent } from 'vue';
import { UserSessionState } from './store';
import usersStore, { UserStateInterface } from './store/user';
import sessionsStorage, { SessionStateInterface } from './store/session';
const plugin: FG_Plugin.Plugin = { const plugin: FG_Plugin.Plugin = {
name: 'User', name: 'User',
@ -12,13 +8,6 @@ const plugin: FG_Plugin.Plugin = {
requiredModules: [], requiredModules: [],
requiredBackendModules: ['auth'], requiredBackendModules: ['auth'],
version: '0.0.1', version: '0.0.1',
store: new Map<
string,
Module<UserStateInterface, UserSessionState> | Module<SessionStateInterface, UserSessionState>
>([
['users', usersStore],
['sessions', sessionsStorage],
]),
widgets: [ widgets: [
{ {
priority: 1, priority: 1,

View File

@ -1,7 +1,12 @@
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import { useMainStore } from 'src/store';
import { computed } from 'vue';
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
{ {
title: 'loadFromStore("users/displayName")', get title() {
return computed(() => useMainStore().user?.display_name || 'Not loaded');
},
icon: 'mdi-account', icon: 'mdi-account',
permissions: ['user'], permissions: ['user'],
route: { path: 'user', name: 'user', component: () => import('../pages/MainPage.vue') }, route: { path: 'user', name: 'user', component: () => import('../pages/MainPage.vue') },

182
src/plugins/user/store.ts Normal file
View File

@ -0,0 +1,182 @@
import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { AxiosError, AxiosResponse } from 'axios';
import { useMainStore } from 'src/store';
export const useUserStore = defineStore({
id: 'users',
state: () => ({
roles: [] as FG.Role[],
users: [] as FG.User[],
permissions: [] as FG.Permission[],
_dirty_users: 0,
_dirty_roles: 0,
}),
getters: {
isDirty() {
return new Date().getTime() - this._dirty_users > 60000;
},
},
actions: {
async getUser(userid: string, force = true) {
const idx = this.users.findIndex((user) => user.userid === userid);
if (force || idx == -1 || this.isDirty) {
try {
const { data } = await api.get<FG.User>(`/users/${userid}`);
if (data.birthday) data.birthday = new Date(data.birthday);
if (idx === -1) this.users.push(data);
else this.users[idx] = data;
return data;
} catch (error) {
if (!error || !('response' in error) || (<AxiosError>error).response?.status !== 404)
throw error;
return null;
}
} else {
return this.users[idx];
}
},
async getUsers(force = true) {
if (force || this.isDirty) {
const { data } = await api.get<FG.User[]>('/users');
data.forEach((user) => {
if (user.birthday) user.birthday = new Date(user.birthday);
});
this.users = data;
this._dirty_users = new Date().getTime();
} else {
return this.users;
}
},
async updateUser(user: FG.User) {
await api.put(`/users/${user.userid}`, user);
const mainStore = useMainStore();
if (user.userid === mainStore.user?.userid) mainStore.user = user;
this._dirty_users = 0;
},
async createUser(user: FG.User) {
const { data } = await api.post<FG.User>('/users', user);
this.users.push(data);
return data;
},
async uploadAvatar(user: FG.User, file: string) {
const formData = new FormData();
formData.append('file', file);
await api.post(`/users/${user.userid}/avatar`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
async getPermissions(force = false) {
if (force || this.permissions.length === 0) {
const { data } = await api.get<FG.Permission[]>('/roles/permissions');
this.permissions = data;
}
return this.permissions;
},
async getRoles(force = false) {
if (force || new Date().getTime() - this._dirty_roles > 60000) {
const { data } = await api.get<FG.Role[]>('/roles');
this.roles = data;
this._dirty_roles = new Date().getTime();
}
return this.roles;
},
async updateRole(role: FG.Role) {
await api.put(`/roles/${role.id}`, role);
const idx = this.roles.findIndex((r) => r.id === role.id);
if (idx != -1) this.roles[idx] = role;
this._dirty_roles = 0;
},
async newRole(role: FG.Role) {
const { data } = await api.post<FG.Role>('/roles', role);
this.roles.push(data);
this._dirty_roles = 0;
return data;
},
async deleteRole(role: FG.Role | number) {
await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`);
this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id));
this._dirty_roles = 0;
},
},
});
export const useSessionStore = defineStore({
id: 'sessions',
state: () => ({}),
getters: {},
actions: {
async getSession(token: string) {
return await api
.get(`/auth/${token}`)
.then(({ data }: AxiosResponse<FG.Session>) => data)
.catch(() => null);
},
async getSessions() {
try {
const { data } = await api.get<FG.Session[]>('/auth');
data.forEach((session) => {
session.expires = new Date(session.expires);
});
const mainStore = useMainStore();
const currentSession = data.find((session) => {
return session.token === mainStore.session?.token;
});
if (currentSession) {
mainStore.session = currentSession;
}
return data;
} catch (error) {
return [] as FG.Session[];
}
},
async deleteSession(token: string) {
const mainStore = useMainStore();
if (token === mainStore.session?.token) return mainStore.logout();
try {
await api.delete(`/auth/${token}`);
return true;
} catch (error) {
if (!error || !('response' in error) || (<AxiosError>error).response?.status != 401)
throw error;
}
return false;
},
async updateSession(lifetime: number, token: string) {
try {
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
data.expires = new Date(data.expires);
const mainStore = useMainStore();
if (mainStore.session?.token == data.token) mainStore.session = data;
return true;
} catch (error) {
return false;
}
},
},
});

View File

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

View File

@ -1,199 +0,0 @@
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { LoginData, LoginResponse } from 'src/plugins/user/models';
import { api } from 'src/boot/axios';
import { AxiosError, AxiosResponse } from 'axios';
import { LocalStorage } from 'quasar';
import { UserSessionState } from '.';
import { Router } from 'src/router';
export interface SessionStateInterface {
currentSession?: FG.Session;
sessions: FG.Session[];
loading: boolean;
}
/**
* Load current session from LocalStorage
* Used when we were already authenticated using this browser
*/
function loadCurrentSession() {
const session = LocalStorage.getItem<FG.Session>('currentSession');
if (session) session.expires = new Date(session.expires);
return session;
}
const state: SessionStateInterface = {
sessions: [],
currentSession: loadCurrentSession() || undefined,
loading: false,
};
const mutations: MutationTree<SessionStateInterface> = {
setCurrentSession(state, session: FG.Session) {
LocalStorage.set('currentSession', session);
state.currentSession = session;
},
clearCurrentSession(state) {
LocalStorage.remove('currentSession');
state.currentSession = undefined;
},
setSessions(state, sessions: FG.Session[]) {
state.sessions = sessions;
},
setLoading(state, value: boolean) {
state.loading = value;
},
updateSession(state, session: FG.Session) {
const index = state.sessions.findIndex((x) => x.token == session.token);
if (index > -1) {
state.sessions[index] = session;
}
},
};
const actions: ActionTree<SessionStateInterface, UserSessionState> = {
/** Used to authenticate the user
* Setting current Session, User and Permissions.
* @param param0 Context
* @param data Credentitals
*/
login({ commit }, data: LoginData) {
return api
.post('/auth', data)
.then((response: AxiosResponse<LoginResponse>) => {
response.data.session.expires = new Date(response.data.session.expires);
commit('setCurrentSession', response.data.session);
commit('users/setCurrentUser', response.data.user, { root: true });
commit('users/setCurrentPermissions', response.data.permissions, {
root: true,
});
})
.catch((error: AxiosError) => {
return Promise.reject(error.response);
});
},
/**
* Logout from current session
* Alias of deleteSession with current session as target
*/
logout({ dispatch, rootState }) {
if (rootState.sessions.currentSession) {
dispatch('deleteSession', rootState.sessions.currentSession.token).catch((error) => {
console.log(error);
void dispatch('clearCurrent', false);
});
} else {
void dispatch('clearCurrent', false);
}
},
/**
* Delete a given session
*/
deleteSession({ commit, dispatch, rootState }, token: string) {
commit('setLoading', true);
api
.delete(`/auth/${token}`)
.then(() => {
if (token === rootState.sessions.currentSession?.token) {
void dispatch('clearCurrent', false);
} else {
dispatch('getSessions').catch((error) => {
throw error;
});
}
})
.catch((error: AxiosError) => {
if (!error.response || error.response.status != 401) throw error;
})
.finally(() => {
commit('setLoading', false);
});
},
/**
* Clear current session and logged in user
*/
clearCurrent({ commit }, redirect = true) {
void Router.push({
name: 'login',
query: redirect ? { redirect: Router.currentRoute.value.fullPath } : {},
params: { logout: 'true' },
}).then(() => {
commit('clearCurrentSession');
commit('users/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);
api
.get('/auth')
.then((response: AxiosResponse<FG.Session[]>) => {
response.data.forEach((session) => {
session.expires = new Date(session.expires);
});
commit('setSessions', response.data);
const currentSession = response.data.find((session: FG.Session) => {
return session.token === state.currentSession?.token;
});
if (currentSession) {
commit('setCurrentSession', currentSession);
}
})
.catch((error) => {
throw error;
})
.finally(() => {
commit('setLoading', false);
});
},
updateSession({ commit, state }, data: { lifetime: number; token: string }) {
commit('setLoading', true);
api
.put(`auth/${data.token}`, { value: data.lifetime })
.then((response: AxiosResponse<FG.Session>) => {
response.data.expires = new Date(response.data.expires);
if (state.currentSession?.token == response.data.token) {
commit('setCurrentSession', response.data);
}
})
.catch((err) => console.log(err))
.finally(() => {
commit('setLoading', false);
});
console.log('updateSession', data);
},
requestPasswordReset({}, data) {
return api.post('/auth/reset', data);
},
resetPassword({}, data) {
return api.post('/auth/reset', data).catch((error: AxiosError) => {
return Promise.reject(error.response);
});
},
};
const getters: GetterTree<SessionStateInterface, UserSessionState> = {
currentSession(state) {
return state.currentSession;
},
sessions(state) {
return state.sessions;
},
loading(state) {
return state.loading;
},
};
const sessionsStore: Module<SessionStateInterface, UserSessionState> = {
namespaced: true,
state,
mutations,
actions,
getters,
};
export default sessionsStore;

View File

@ -1,275 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
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;
currentPermissions: FG.Permission[];
users: FG.User[];
roles: FG.Role[];
permissions: FG.Permission[];
loading: number;
}
function loadUserFromLocalStorage() {
const user = SessionStorage.getItem<FG.User>('currentUser') || undefined;
if (user && user.birthday && typeof user.birthday === 'string')
user.birthday = new Date(user.birthday);
return user;
}
const state: UserStateInterface = {
users: [],
roles: [],
permissions: [],
currentUser: loadUserFromLocalStorage(),
currentPermissions: SessionStorage.getItem<FG.Permission[]>('currentPermissions') || [],
loading: 0,
};
const mutations: MutationTree<UserStateInterface> = {
setCurrentUser(state, data: FG.User) {
if (typeof data.birthday === 'string') data.birthday = new Date(data.birthday);
SessionStorage.set('currentUser', data);
state.currentUser = data;
},
setCurrentPermissions(state, data: FG.Permission[]) {
SessionStorage.set('currentPermissions', data);
state.currentPermissions = data;
},
clearCurrentUser(state) {
SessionStorage.remove('currentUser');
SessionStorage.remove('currentPermissions');
state.currentUser = undefined;
state.currentPermissions = [];
},
setUsers(state, data: FG.User[]) {
state.users = data;
},
setUser(state, data: FG.User) {
const index = state.users.findIndex((x) => x.userid === data.userid);
if (index > -1) state.users[index] = data;
else state.users.push(data);
},
setRoles(state, data: FG.Role[]) {
state.roles = data;
},
addRole(state, data: FG.Role) {
state.roles.push(data);
},
updateRole(state, data: FG.Role) {
const idx = state.roles.findIndex((role) => role.id === data.id);
if (idx >= 0) {
state.roles[idx].name = data.name;
state.roles[idx].permissions = data.permissions;
}
},
setPermissions(state, data: FG.Permission[]) {
state.permissions = data;
},
setLoading(state, data = true) {
if (data) state.loading += 1;
else state.loading -= 1;
},
};
const actions: ActionTree<UserStateInterface, UserSessionState> = {
getCurrentUser({ commit, rootState }) {
if (rootState.sessions.currentSession) {
commit('setLoading');
api
.get(`/users/${rootState.sessions.currentSession.userid}`)
.then((response: AxiosResponse<CurrentUserResponse>) => {
commit('setCurrentUser', response.data);
commit('setCurrentPermissions', response.data.permissions);
})
.catch((err) => {
console.warn(err);
})
.finally(() => {
commit('setLoading', false);
});
} else {
console.debug('User not logged in, can not get current_user.');
}
},
getUsers({ commit }) {
commit('setLoading');
api
.get('/users')
.then((response: AxiosResponse<FG.User[]>) => {
response.data.forEach((user) => {
if (user.birthday) user.birthday = new Date(user.birthday);
});
commit('setUsers', response.data);
})
.catch((err) => {
console.warn(err);
})
.finally(() => {
commit('setLoading', false);
});
},
updateUser({ commit, state, dispatch }, data: FG.User) {
commit('setLoading');
api
.put(`/users/${data.userid}`, data)
.then(() => {
if (state.currentUser && state.currentUser.userid === data.userid)
void dispatch('getCurrentUser');
else void dispatch('getUsers');
})
.catch((error) => {
console.log(error);
})
.finally(() => {
commit('setLoading', false);
});
},
uploadAvatar({ commit }, payload: { user: FG.User; file: string }) {
commit('setLoading');
const formData = new FormData();
formData.append('file', payload.file);
return api
.post(`/users/${payload.user.userid}/avatar`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.catch((error: AxiosError) => {
return Promise.reject(error.response);
})
.finally(() => {
commit('setLoading', false);
});
},
setUser({ commit, state, dispatch }, data: FG.User) {
commit('setLoading');
api
.post('users', data)
.then(() => {
if (state.currentUser && state.currentUser.userid === data.userid)
void dispatch('getCurrentUser');
else void dispatch('getUsers');
})
.catch((error) => {
console.warn(error);
})
.finally(() => {
commit('setLoading', false);
});
},
getRoles({ commit, state }, force = false) {
if (!force && state.roles.length > 0) return;
commit('setLoading');
api
.get('/roles')
.then((response: AxiosResponse<FG.Role[]>) => {
commit('setRoles', response.data);
})
.finally(() => commit('setLoading', false));
},
updateRole({ commit }, data: FG.Role) {
commit('setLoading');
api
.put(`/roles/${data.id}`, data)
.then(() => {
commit('updateRole', data);
})
.finally(() => {
commit('setLoading', false);
});
},
newRole({ commit }, data: FG.Role) {
commit('setLoading');
return api
.post('/roles', data)
.then((response: AxiosResponse<FG.Role>) => {
commit('addRole', response.data);
return Promise.resolve(response.data);
})
.finally(() => {
commit('setLoading', false);
});
},
deleteRole({ commit, state }, data: FG.Role) {
commit('setLoading');
api
.delete(`/roles/${data.id}`)
.then(() => {
commit(
'setRoles',
state.roles.filter((value) => value.id !== data.id)
);
})
.finally(() => {
commit('setLoading', false);
});
},
getPermissions({ commit, state }, force = false) {
if (!force && state.permissions.length > 0) return;
commit('setLoading');
api
.get('/roles/permissions')
.then((response: AxiosResponse<FG.Role[]>) => {
commit('setPermissions', response.data);
})
.finally(() => commit('setLoading', false));
},
getUser({ commit, getters }, data: { userid: string; force?: boolean }) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const user = <FG.User | undefined>getters['getUser'](data.userid);
if (user === undefined || data.force === true) {
return api.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => {
commit('setUser', response.data);
return response.data;
});
} else {
return Promise.resolve(user);
}
},
};
const getters: GetterTree<UserStateInterface, UserSessionState> = {
getUser: (state) => (userid: string) => {
const user = state.users.filter((usr) => usr.userid === userid);
return user.length > 0 ? user[0] : undefined;
},
currentUser({ currentUser }) {
return currentUser;
},
users({ users }) {
return users;
},
loading({ loading }) {
return loading > 0;
},
displayName({ currentUser }) {
return currentUser?.display_name;
},
roles({ roles }): string[] {
return roles.map((role) => role.name).flat();
},
};
const usersStore: Module<UserStateInterface, UserSessionState> = {
namespaced: true,
actions,
getters,
mutations,
state,
};
export default usersStore;

View File

@ -1,11 +1,7 @@
import { store } from 'quasar/wrappers'; import { store } from 'quasar/wrappers';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { UserStateInterface } from 'src/plugins/user/store/user';
import { SessionStateInterface } from 'src/plugins/user/store/session';
export interface StateInterface { export interface StateInterface {
users: UserStateInterface;
sessions: SessionStateInterface;
[key: string]: unknown; [key: string]: unknown;
} }
@ -20,3 +16,95 @@ export default store(function (/* { ssrContext } */) {
return Store; return Store;
}); });
import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { AxiosResponse } from 'axios';
import { LocalStorage } from 'quasar';
import { useUserStore, useSessionStore } from 'src/plugins/user/store';
function loadCurrentSession() {
const session = LocalStorage.getItem<FG.Session>('session');
if (session) session.expires = new Date(session.expires);
return session || undefined;
}
export const useMainStore = defineStore({
id: 'main',
state: () => ({
session: loadCurrentSession(),
user: undefined as FG.User | undefined,
}),
getters: {
loggedIn() {
return this.session !== undefined;
},
currentUser() {
if (this.user === undefined) throw 'Not logged in, this should not be called';
return this.user;
},
permissions() {
return this.user?.permissions || [];
},
},
actions: {
/** Ininitalize store from saved session
* Updates session and loads current user
*/
async init() {
if (this.session) {
const sessionStore = useSessionStore();
const session = await sessionStore.getSession(this.session.token);
if (session) {
this.session = this.session;
const userStore = useUserStore();
const user = await userStore.getUser(this.session.userid);
if (user) this.user = user;
}
}
},
async login(userid: string, password: string) {
try {
const { data } = await api.post<FG.Session>('/auth', { userid, password });
this.session = data;
this.session.expires = new Date(this.session.expires);
LocalStorage.set('session', this.session);
return true;
} catch ({ response }) {
return (<AxiosResponse | undefined>response)?.status || false;
}
},
logout() {
if (!this.session) return false;
void api.delete(`/auth/${this.session.token}`);
this.$patch({
session: undefined,
user: undefined,
});
LocalStorage.remove('session');
return true;
},
async requestReset(userid: string) {
return await api
.post('/auth/reset', { userid })
.then(() => true)
.catch(() => false);
},
async resetPassword(token: string, password: string) {
return await api
.post('/auth/reset', { token, password })
.then(() => true)
.catch(({ response }) =>
response && 'status' in response ? (<AxiosResponse>response).status : false
);
},
},
});

View File

@ -1,19 +1,16 @@
import { useStore } from 'vuex'; import { useMainStore } from 'src/store';
import { UserSessionState } from 'src/plugins/user/store';
export function hasPermission(permission: string) { export function hasPermission(permission: string) {
const store = useStore<UserSessionState>(); const store = useMainStore();
return store.state.users.currentPermissions.includes(permission); return store.permissions.includes(permission);
} }
export function hasPermissions(needed: string[]) { export function hasPermissions(needed: string[]) {
const store = useStore<UserSessionState>(); const store = useMainStore();
const permissions = store.state.users.currentPermissions; return needed.every((value) => store.permissions.includes(value));
return needed.every((value) => permissions.includes(value));
} }
export function hasSomePermissions(needed: string[]) { export function hasSomePermissions(needed: string[]) {
const store = useStore<UserSessionState>(); const store = useMainStore();
const permissions = store.state.users.currentPermissions; return needed.some((value) => store.permissions.includes(value));
return needed.some((value) => permissions.includes(value));
} }

View File

@ -7497,6 +7497,11 @@ pify@^5.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==
pinia@^2.0.0-alpha.7:
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.0-alpha.7.tgz#3b55af0185a45e6e1a75ba90f6da3e1a61623767"
integrity sha512-IXPG+4elwC+0h5r3FZUhcH+XwywMN+f9uxoMxzapz5ywBfo7BeE48wpC1o+81qnil5m3FhBOjMKZeS7t0t6ljw==
pinkie-promise@^2.0.0: pinkie-promise@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"