Reworked user and session store, added Admin function for user.
* Sync Login with backend * Split Main into MainUserSettins and Settings * Added AdminSetting to change other users, added UserSelector Component for selecting users (can be reused for other stuff ;-) ). * Split hasPermission into helper file for code reuse
This commit is contained in:
		
							parent
							
								
									5c11e02b2c
								
							
						
					
					
						commit
						8689e84d47
					
				| 
						 | 
				
			
			@ -5,16 +5,8 @@ import { Store } from 'vuex';
 | 
			
		|||
 | 
			
		||||
export default boot<Store<StateInterface>>(({ router, store }) => {
 | 
			
		||||
  router.beforeEach((to, from, next) => {
 | 
			
		||||
    const user = store.state.user.currentUser;
 | 
			
		||||
    const session = store.state.session.currentSession;
 | 
			
		||||
 | 
			
		||||
    let permissions: string[] = [];
 | 
			
		||||
    if (user) {
 | 
			
		||||
      user.roles.forEach(role => {
 | 
			
		||||
        permissions = permissions.concat(role.permissions);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (to.name != 'login') {
 | 
			
		||||
      if (!session || session.expires <= new Date()) {
 | 
			
		||||
        store.dispatch('session/logout').catch(error => {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +24,7 @@ export default boot<Store<StateInterface>>(({ router, store }) => {
 | 
			
		|||
              return (<{ permissions: FG.Permission[] }>(
 | 
			
		||||
                record.meta
 | 
			
		||||
              )).permissions.every((permission: string) => {
 | 
			
		||||
                return permissions.includes(permission);
 | 
			
		||||
                return store.state.user.currentPermissions.includes(permission);
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@
 | 
			
		|||
import { computed, defineComponent } from '@vue/composition-api';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
import { hasPermissions } from 'src/components/permission';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'EssentialLink',
 | 
			
		||||
| 
						 | 
				
			
			@ -65,17 +66,9 @@ export default defineComponent({
 | 
			
		|||
      return props.title;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const hasPermissions = computed(() => {
 | 
			
		||||
      let permissions = props.permissions;
 | 
			
		||||
      if (permissions) {
 | 
			
		||||
        return (<string[]>permissions).every(permission => {
 | 
			
		||||
          return (<{ 'user/permissions': string[] }>(
 | 
			
		||||
            (<Store<StateInterface>>root.$store).getters
 | 
			
		||||
          ))['user/permissions'].includes(permission);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    });
 | 
			
		||||
    const isGranted = computed(() =>
 | 
			
		||||
      hasPermissions(props.permissions || [], root.$store)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return { realTitle: title, hasPermissions };
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,12 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-btn flat dense :icon="icon" :to="{ name: link }" v-if="hasPermissions" />
 | 
			
		||||
  <q-btn flat dense :icon="icon" :to="{ name: link }" v-if="isGranted" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent } from '@vue/composition-api';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
import { hasPermissions } from 'src/components/permission';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'ShortCutLink',
 | 
			
		||||
| 
						 | 
				
			
			@ -23,18 +24,10 @@ export default defineComponent({
 | 
			
		|||
    }
 | 
			
		||||
  },
 | 
			
		||||
  setup(props, { root }) {
 | 
			
		||||
    const hasPermissions = computed(() => {
 | 
			
		||||
      let permissions = props.permissions;
 | 
			
		||||
      if (permissions) {
 | 
			
		||||
        return (<string[]>permissions).every(permission => {
 | 
			
		||||
          return (<{ 'user/permissions': string[] }>(
 | 
			
		||||
            (<Store<StateInterface>>root.$store).getters
 | 
			
		||||
          ))['user/permissions'].includes(permission);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    });
 | 
			
		||||
    return { hasPermissions };
 | 
			
		||||
    const isGranted = computed(() =>
 | 
			
		||||
      hasPermissions(props.permissions || [], root.$store)
 | 
			
		||||
    );
 | 
			
		||||
    return { isGranted };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
 | 
			
		||||
export function hasPermission(
 | 
			
		||||
  permission: string,
 | 
			
		||||
  store: Store<StateInterface>
 | 
			
		||||
) {
 | 
			
		||||
  return store.state.user.currentPermissions.includes(permission);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hasPermissions(needed: string[], store: Store<StateInterface>) {
 | 
			
		||||
  const permissions = store.state.user.currentPermissions;
 | 
			
		||||
  return needed.every(value => permissions.includes(value));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-select
 | 
			
		||||
    filled
 | 
			
		||||
    label="Benutzer"
 | 
			
		||||
    @input="updated"
 | 
			
		||||
    v-model="user"
 | 
			
		||||
    :options="users"
 | 
			
		||||
    option-label="display_name"
 | 
			
		||||
    option-value="userid"
 | 
			
		||||
    map-options
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, ref } from '@vue/composition-api';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  user: FG.User;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'UserSelector',
 | 
			
		||||
  props: ['user'],
 | 
			
		||||
  setup(props: Props, { root, emit }) {
 | 
			
		||||
    const store = <Store<StateInterface>>root.$store;
 | 
			
		||||
    const users = computed(() => store.state.user.users);
 | 
			
		||||
    const user = ref(props.user);
 | 
			
		||||
    const updated = (value: FG.User) => {
 | 
			
		||||
      emit('update:user', value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      user,
 | 
			
		||||
      updated,
 | 
			
		||||
      users
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,191 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-card class="col-12">
 | 
			
		||||
    <q-linear-progress indeterminate rounded color="primary" v-if="loading" />
 | 
			
		||||
    <q-form @submit="save" @reset="reset">
 | 
			
		||||
      <q-card-section class="fit row justify-start content-center items-center">
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="Vorname"
 | 
			
		||||
          :rules="[notEmpty]"
 | 
			
		||||
          v-model="firstname"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="Nachname"
 | 
			
		||||
          :rules="[notEmpty]"
 | 
			
		||||
          v-model="lastname"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="Benutzername"
 | 
			
		||||
          readonly
 | 
			
		||||
          :value="user.userid"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="E-Mail"
 | 
			
		||||
          :rules="[isEmail, notEmpty]"
 | 
			
		||||
          v-model="mail"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="Display Name"
 | 
			
		||||
          :rules="[notEmpty]"
 | 
			
		||||
          v-model="display_name"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-select
 | 
			
		||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
          label="Rollen"
 | 
			
		||||
          readonly
 | 
			
		||||
          v-model="user.roles"
 | 
			
		||||
          :options="user.roles"
 | 
			
		||||
          filled
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:selected-item="scope">
 | 
			
		||||
            <q-chip v-for="(item, index) in scope.opt" :key="'item' + index">
 | 
			
		||||
              {{ item.name }}
 | 
			
		||||
            </q-chip>
 | 
			
		||||
          </template>
 | 
			
		||||
        </q-select>
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
      <q-separator />
 | 
			
		||||
      <q-card-section class="fit row justify-start content-center items-center">
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
          label="Password"
 | 
			
		||||
          type="password"
 | 
			
		||||
          hint="Password muss immer eingetragen werden"
 | 
			
		||||
          :rules="[notEmpty]"
 | 
			
		||||
          v-model="password"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
          label="Neues Password"
 | 
			
		||||
          type="password"
 | 
			
		||||
          v-model="new_password"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
        <q-input
 | 
			
		||||
          class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
          label="Wiederhole neues Password"
 | 
			
		||||
          type="password"
 | 
			
		||||
          :disable="new_password.length == 0"
 | 
			
		||||
          :rules="[samePassword]"
 | 
			
		||||
          v-model="new_password2"
 | 
			
		||||
          filled
 | 
			
		||||
        />
 | 
			
		||||
      </q-card-section>
 | 
			
		||||
      <q-card-actions align="right">
 | 
			
		||||
        <q-btn label="test" @click="$store.dispatch('user/getUser')" />
 | 
			
		||||
        <q-btn label="Reset" type="reset" />
 | 
			
		||||
        <q-btn color="primary" type="submit" label="Speichern" />
 | 
			
		||||
      </q-card-actions>
 | 
			
		||||
    </q-form>
 | 
			
		||||
  </q-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, ref } from '@vue/composition-api';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'Main',
 | 
			
		||||
  setup(_, { root }) {
 | 
			
		||||
    const store = <Store<StateInterface>>root.$store;
 | 
			
		||||
 | 
			
		||||
    const user = computed(() => <FG.User>store.state.user.currentUser);
 | 
			
		||||
 | 
			
		||||
    const firstname = ref(user.value?.firstname);
 | 
			
		||||
    const lastname = ref(user.value?.lastname);
 | 
			
		||||
    const mail = ref(user.value?.mail);
 | 
			
		||||
    const display_name = ref(user.value?.display_name);
 | 
			
		||||
 | 
			
		||||
    const password = ref('');
 | 
			
		||||
    const new_password = ref('');
 | 
			
		||||
    const new_password2 = ref('');
 | 
			
		||||
 | 
			
		||||
    function save() {
 | 
			
		||||
      let change_values: { [index: string]: string } = {
 | 
			
		||||
        firstname: firstname.value,
 | 
			
		||||
        lastname: lastname.value,
 | 
			
		||||
        mail: mail.value,
 | 
			
		||||
        display_name: display_name.value
 | 
			
		||||
      };
 | 
			
		||||
      Object.keys(change_values).forEach(key => {
 | 
			
		||||
        if (
 | 
			
		||||
          change_values[key] === (<{ [index: string]: any }>user.value)[key]
 | 
			
		||||
        ) {
 | 
			
		||||
          delete change_values[key];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      change_values = Object.assign(change_values, {
 | 
			
		||||
        password: password.value
 | 
			
		||||
      });
 | 
			
		||||
      if (new_password.value != '') {
 | 
			
		||||
        change_values = Object.assign(change_values, {
 | 
			
		||||
          new_password: new_password.value
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      store.dispatch('user/updateUser', change_values).catch(error => {
 | 
			
		||||
        console.warn(error);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function reset() {
 | 
			
		||||
      firstname.value = user.value.firstname;
 | 
			
		||||
      lastname.value = user.value.lastname;
 | 
			
		||||
      mail.value = user.value.mail;
 | 
			
		||||
      display_name.value = user.value.display_name;
 | 
			
		||||
      password.value = '';
 | 
			
		||||
      new_password.value = '';
 | 
			
		||||
      new_password2.value = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function samePassword(val: string) {
 | 
			
		||||
      return val == new_password.value || 'Passwörter sind nicht identisch!';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function notEmpty(val: string) {
 | 
			
		||||
      return !!val || 'Feld darf nicht leer sein!';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function isEmail(val: string | null) {
 | 
			
		||||
      return (
 | 
			
		||||
        !val ||
 | 
			
		||||
        /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) ||
 | 
			
		||||
        'E-Mail ist nicht valide.'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const loading = computed(() => {
 | 
			
		||||
      return (
 | 
			
		||||
        store.state.user.getUserLoading || store.state.user.updateUserLoading
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      user,
 | 
			
		||||
      firstname,
 | 
			
		||||
      lastname,
 | 
			
		||||
      mail,
 | 
			
		||||
      display_name,
 | 
			
		||||
      password,
 | 
			
		||||
      new_password,
 | 
			
		||||
      new_password2,
 | 
			
		||||
      samePassword,
 | 
			
		||||
      isEmail,
 | 
			
		||||
      notEmpty,
 | 
			
		||||
      save,
 | 
			
		||||
      reset,
 | 
			
		||||
      loading
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,194 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-form @submit="save" @reset="reset">
 | 
			
		||||
    <q-linear-progress indeterminate rounded color="primary" v-if="loading" />
 | 
			
		||||
    <q-card-section class="fit row justify-start content-center items-center">
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="Vorname"
 | 
			
		||||
        :rules="[notEmpty]"
 | 
			
		||||
        v-model="props.user.firstname"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="Nachname"
 | 
			
		||||
        :rules="[notEmpty]"
 | 
			
		||||
        v-model="props.user.lastname"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="Benutzername"
 | 
			
		||||
        readonly
 | 
			
		||||
        :value="props.user.userid"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="E-Mail"
 | 
			
		||||
        :rules="[isEmail, notEmpty]"
 | 
			
		||||
        v-model="props.user.mail"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="Display Name"
 | 
			
		||||
        :rules="[notEmpty]"
 | 
			
		||||
        v-model="props.user.display_name"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-select
 | 
			
		||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
			
		||||
        label="Rollen"
 | 
			
		||||
        filled
 | 
			
		||||
        multiple
 | 
			
		||||
        use-chips
 | 
			
		||||
        v-model="props.user.roles"
 | 
			
		||||
        :readonly="() => canSetRoles()"
 | 
			
		||||
        :options="allRoles"
 | 
			
		||||
        option-label="name"
 | 
			
		||||
        option-value="name"
 | 
			
		||||
      />
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
    <q-separator />
 | 
			
		||||
    <q-card-section class="fit row justify-start content-center items-center">
 | 
			
		||||
      <q-input
 | 
			
		||||
        v-if="isCurrentUser"
 | 
			
		||||
        class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
        label="Password"
 | 
			
		||||
        type="password"
 | 
			
		||||
        hint="Password muss immer eingetragen werden"
 | 
			
		||||
        :rules="[notEmpty]"
 | 
			
		||||
        v-model="password"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
        label="Neues Password"
 | 
			
		||||
        type="password"
 | 
			
		||||
        v-model="new_password"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
      <q-input
 | 
			
		||||
        class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
 | 
			
		||||
        label="Wiederhole neues Password"
 | 
			
		||||
        type="password"
 | 
			
		||||
        :disable="new_password.length == 0"
 | 
			
		||||
        :rules="[samePassword]"
 | 
			
		||||
        v-model="new_password2"
 | 
			
		||||
        filled
 | 
			
		||||
      />
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
    <q-card-actions align="right">
 | 
			
		||||
      <q-btn label="Reset" type="reset" />
 | 
			
		||||
      <q-btn color="primary" type="submit" label="Speichern" />
 | 
			
		||||
    </q-card-actions>
 | 
			
		||||
  </q-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
  computed,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  ref,
 | 
			
		||||
  onBeforeMount
 | 
			
		||||
} from '@vue/composition-api';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
import { hasPermission } from 'src/components/permission';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  user?: FG.User;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'MainUserSettings',
 | 
			
		||||
  props: ['user'],
 | 
			
		||||
  setup(props: Props, { root }) {
 | 
			
		||||
    const store = <Store<StateInterface>>root.$store;
 | 
			
		||||
 | 
			
		||||
    onBeforeMount(() => {
 | 
			
		||||
      store.dispatch('user/getRoles', false).catch(error => {
 | 
			
		||||
        console.warn(error);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const isCurrentUser = computed(
 | 
			
		||||
      () => props.user?.userid === store.state.user.currentUser?.userid
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const canSetRoles = computed(() => hasPermission('users_set_roles', store));
 | 
			
		||||
 | 
			
		||||
    const oldUser = computed(() => {
 | 
			
		||||
      if (isCurrentUser.value) return <FG.User>store.state.user.currentUser;
 | 
			
		||||
      else
 | 
			
		||||
        return store.state.user.users.filter(user => {
 | 
			
		||||
          user.userid === props.user?.userid;
 | 
			
		||||
        })[0];
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const allRoles = computed(() =>
 | 
			
		||||
      store.state.user.roles.map(role => role.name)
 | 
			
		||||
    );
 | 
			
		||||
    const password = ref('');
 | 
			
		||||
    const new_password = ref('');
 | 
			
		||||
    const new_password2 = ref('');
 | 
			
		||||
 | 
			
		||||
    function save() {
 | 
			
		||||
      let changed = <FG.User>props.user;
 | 
			
		||||
      changed = Object.assign(changed, {
 | 
			
		||||
        password: password.value
 | 
			
		||||
      });
 | 
			
		||||
      if (new_password.value != '') {
 | 
			
		||||
        changed = Object.assign(changed, {
 | 
			
		||||
          new_password: new_password.value
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      store.dispatch('user/updateUser', changed).catch(error => {
 | 
			
		||||
        console.warn(error);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function reset() {
 | 
			
		||||
      props.user = oldUser.value;
 | 
			
		||||
      password.value = '';
 | 
			
		||||
      new_password.value = '';
 | 
			
		||||
      new_password2.value = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function samePassword(val: string) {
 | 
			
		||||
      return val == new_password.value || 'Passwörter sind nicht identisch!';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function notEmpty(val: string) {
 | 
			
		||||
      return !!val || 'Feld darf nicht leer sein!';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function isEmail(val: string | null) {
 | 
			
		||||
      return (
 | 
			
		||||
        !val ||
 | 
			
		||||
        /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) ||
 | 
			
		||||
        'E-Mail ist nicht valide.'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const loading = computed(() => store.state.user.loading > 0);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      props,
 | 
			
		||||
      allRoles,
 | 
			
		||||
      canSetRoles,
 | 
			
		||||
      password,
 | 
			
		||||
      new_password,
 | 
			
		||||
      new_password2,
 | 
			
		||||
      samePassword,
 | 
			
		||||
      isCurrentUser,
 | 
			
		||||
      isEmail,
 | 
			
		||||
      notEmpty,
 | 
			
		||||
      save,
 | 
			
		||||
      reset,
 | 
			
		||||
      loading
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <q-page
 | 
			
		||||
      padding
 | 
			
		||||
      class="fit row justify-center content-center items-center q-gutter-sm"
 | 
			
		||||
    >
 | 
			
		||||
      <q-card class="col-12">
 | 
			
		||||
        <q-card-section
 | 
			
		||||
          class="fit row justify-start content-center items-center"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="col-xs-12 col-sm-6 text-center text-h6">
 | 
			
		||||
            Benutzereinstellungen
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col-xs-12 col-sm-6 q-pa-sm">
 | 
			
		||||
            <UserSelector :user="user" @update:user="userUpdated" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <MainUserSettings :user="user" />
 | 
			
		||||
      </q-card>
 | 
			
		||||
    </q-page>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onBeforeMount, ref } from '@vue/composition-api';
 | 
			
		||||
import UserSelector from '../components/UserSelector.vue';
 | 
			
		||||
import MainUserSettings from '../components/settings/MainUserSettings.vue';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'AdminSettings',
 | 
			
		||||
  components: { UserSelector, MainUserSettings },
 | 
			
		||||
  setup(_, { root }) {
 | 
			
		||||
    const store = <Store<StateInterface>>root.$store;
 | 
			
		||||
 | 
			
		||||
    onBeforeMount(() => {
 | 
			
		||||
      store.dispatch('user/getUsers').catch(error => console.warn(error));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const user = ref(<FG.User>store.state.user.currentUser);
 | 
			
		||||
 | 
			
		||||
    // can be dropped with VUE3
 | 
			
		||||
    const userUpdated = (value: FG.User) => {
 | 
			
		||||
      user.value = value;
 | 
			
		||||
      console.log(value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      user,
 | 
			
		||||
      userUpdated
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,42 +3,42 @@
 | 
			
		|||
    <q-page
 | 
			
		||||
      padding
 | 
			
		||||
      class="fit row justify-center content-center items-center q-gutter-sm"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        class="fit row justify-center content-center items-center q-gutter-sm"
 | 
			
		||||
    >
 | 
			
		||||
      <circular-progress v-if="sessionsLoading" />
 | 
			
		||||
        <div class="col-12 text-left text-h6">
 | 
			
		||||
          Allgemeine Einstellungen:
 | 
			
		||||
        </div>
 | 
			
		||||
        <Main />
 | 
			
		||||
        <div class="col-12 text-left text-h6">
 | 
			
		||||
          Aktive Sessions:
 | 
			
		||||
        </div>
 | 
			
		||||
      <q-card class="col-12">
 | 
			
		||||
        <q-card-section
 | 
			
		||||
          class="fit row justify-start content-center items-center"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="col-12 text-center text-h6">Benutzereinstellungen</div>
 | 
			
		||||
        </q-card-section>
 | 
			
		||||
        <MainUserSettings :user="currentUser" />
 | 
			
		||||
      </q-card>
 | 
			
		||||
      <div class="col-12 text-left text-h6">Aktive Sessions:</div>
 | 
			
		||||
      <sessions
 | 
			
		||||
        v-for="(session, index) in sessions"
 | 
			
		||||
        :key="'session' + index"
 | 
			
		||||
        :session="session"
 | 
			
		||||
      />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <q-btn label="show sessions" @click="showRootGetters" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </q-page>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, onBeforeMount } from '@vue/composition-api';
 | 
			
		||||
import {
 | 
			
		||||
  computed,
 | 
			
		||||
  defineComponent,
 | 
			
		||||
  onBeforeMount,
 | 
			
		||||
  ref
 | 
			
		||||
} from '@vue/composition-api';
 | 
			
		||||
import CircularProgress from 'components/loading/CircularProgress.vue';
 | 
			
		||||
import Sessions from '../components/settings/Sessions.vue';
 | 
			
		||||
import Main from '../components/settings/Main.vue';
 | 
			
		||||
import MainUserSettings from '../components/settings/MainUserSettings.vue';
 | 
			
		||||
import { Store } from 'vuex';
 | 
			
		||||
import { StateInterface } from 'src/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  // name: 'PageName'
 | 
			
		||||
  components: { CircularProgress, Sessions, Main },
 | 
			
		||||
  components: { CircularProgress, Sessions, MainUserSettings },
 | 
			
		||||
  setup(_, { root }) {
 | 
			
		||||
    const store = <Store<StateInterface>>root.$store;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,21 +47,13 @@ export default defineComponent({
 | 
			
		|||
        console.warn(error);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const currentUser = ref(<FG.User>store.state.user.currentUser);
 | 
			
		||||
    const sessions = computed(() => store.state.session.sessions);
 | 
			
		||||
 | 
			
		||||
    function showRootGetters() {
 | 
			
		||||
      console.log(sessions.value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const sessionsLoading = computed(
 | 
			
		||||
      () =>
 | 
			
		||||
        store.state.session.loading ||
 | 
			
		||||
        store.state.user.getUserLoading ||
 | 
			
		||||
        store.state.user.updateUserLoading
 | 
			
		||||
    );
 | 
			
		||||
    const sessionsLoading = computed(() => store.state.session.loading);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      showRootGetters,
 | 
			
		||||
      currentUser,
 | 
			
		||||
      sessionsLoading,
 | 
			
		||||
      sessions
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,15 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
 | 
			
		|||
        shortcut: true,
 | 
			
		||||
        meta: { permissions: ['user'] },
 | 
			
		||||
        component: () => import('../pages/Settings.vue')
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        title: 'Admin',
 | 
			
		||||
        icon: 'mdi-cog',
 | 
			
		||||
        path: 'admin',
 | 
			
		||||
        name: 'admin-settings',
 | 
			
		||||
        shortcut: false,
 | 
			
		||||
        meta: { permissions: ['users_edit_other'] },
 | 
			
		||||
        component: () => import('../pages/AdminSettings.vue')
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ 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 { AxiosResponse } from 'axios';
 | 
			
		||||
import { AxiosError, AxiosResponse } from 'axios';
 | 
			
		||||
import { Router } from 'src/router';
 | 
			
		||||
import { LocalStorage, Loading } from 'quasar';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,10 +12,15 @@ export interface SessionInterface {
 | 
			
		|||
  loading: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function loadFromLocal() {
 | 
			
		||||
  const session = LocalStorage.getItem<FG.Session>('currentSession');
 | 
			
		||||
  if (session) session.expires = new Date(session.expires);
 | 
			
		||||
  return session;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const state: SessionInterface = {
 | 
			
		||||
  sessions: [],
 | 
			
		||||
  currentSession:
 | 
			
		||||
    LocalStorage.getItem<FG.Session>('currentSession') || undefined,
 | 
			
		||||
  currentSession: loadFromLocal() || undefined,
 | 
			
		||||
  loading: false
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,12 +52,13 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
 | 
			
		|||
        response.data.session.expires = new Date(response.data.session.expires);
 | 
			
		||||
        commit('setCurrentSession', response.data.session);
 | 
			
		||||
        commit('user/setCurrentUser', response.data.user, { root: true });
 | 
			
		||||
        void Router.push({ name: 'user-main' });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        console.exception(error);
 | 
			
		||||
        commit('user/setCurrentPermissions', response.data.permissions, {
 | 
			
		||||
          root: true
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => console.warn(error))
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        void Router.push({ name: 'user-main' });
 | 
			
		||||
        Loading.hide();
 | 
			
		||||
      });
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +76,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
 | 
			
		|||
  /**
 | 
			
		||||
   * Delete a given session
 | 
			
		||||
   */
 | 
			
		||||
  deleteSession({ commit, rootState }, token: string | null) {
 | 
			
		||||
  deleteSession({ commit, dispatch, rootState }, token: string | null) {
 | 
			
		||||
    if (token === null) return;
 | 
			
		||||
 | 
			
		||||
    commit('setLoading', true);
 | 
			
		||||
| 
						 | 
				
			
			@ -78,17 +84,23 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
 | 
			
		|||
      .delete(`/auth/${token}`)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        if (token === rootState.session.currentSession?.token) {
 | 
			
		||||
          commit('clearCurrentSession');
 | 
			
		||||
          commit('user/clearCurrentUser', null, { root: true });
 | 
			
		||||
          void Router.push({ name: 'login' });
 | 
			
		||||
          void dispatch('clearup');
 | 
			
		||||
        } else {
 | 
			
		||||
          commit('getSessions');
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error: AxiosError) => {
 | 
			
		||||
        if (!error.response || error.response.status != 401) throw error;
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => {
 | 
			
		||||
        commit('setLoading', false);
 | 
			
		||||
      });
 | 
			
		||||
  },
 | 
			
		||||
  clearup({ commit }) {
 | 
			
		||||
    commit('clearCurrentSession');
 | 
			
		||||
    commit('user/clearCurrentUser', null, { root: true });
 | 
			
		||||
    void Router.push({ name: 'login' });
 | 
			
		||||
  },
 | 
			
		||||
  /**
 | 
			
		||||
   * Get all sessions from current User
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,19 +3,25 @@ import { StateInterface } from 'src/store';
 | 
			
		|||
import { axios } from 'boot/axios';
 | 
			
		||||
import { AxiosResponse } from 'axios';
 | 
			
		||||
import { SessionStorage } from 'quasar';
 | 
			
		||||
import { CurrentUserResponse } from 'src/plugins/user/models';
 | 
			
		||||
 | 
			
		||||
export interface UserStateInterface {
 | 
			
		||||
  updateUserLoading: boolean;
 | 
			
		||||
  getUserLoading: boolean;
 | 
			
		||||
  currentUser?: FG.User;
 | 
			
		||||
  currentPermissions: FG.Permission[];
 | 
			
		||||
  users: FG.User[];
 | 
			
		||||
  roles: FG.Role[];
 | 
			
		||||
  permissions: FG.Permission[];
 | 
			
		||||
  loading: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const state: UserStateInterface = {
 | 
			
		||||
  users: [],
 | 
			
		||||
  roles: [],
 | 
			
		||||
  permissions: [],
 | 
			
		||||
  currentUser: SessionStorage.getItem<FG.User>('currentUser') || undefined,
 | 
			
		||||
  updateUserLoading: false,
 | 
			
		||||
  getUserLoading: false
 | 
			
		||||
  currentPermissions:
 | 
			
		||||
    SessionStorage.getItem<FG.Permission[]>('currentPermissions') || [],
 | 
			
		||||
  loading: 0
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mutations: MutationTree<UserStateInterface> = {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,35 +29,46 @@ const mutations: MutationTree<UserStateInterface> = {
 | 
			
		|||
    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;
 | 
			
		||||
  },
 | 
			
		||||
  setLoading(
 | 
			
		||||
    state,
 | 
			
		||||
    data: { key: 'updateUserLoading' | 'getUserLoading'; data: boolean }
 | 
			
		||||
  ) {
 | 
			
		||||
    state[data.key] = data.data;
 | 
			
		||||
  setRoles(state, data: FG.Role[]) {
 | 
			
		||||
    state.roles = data;
 | 
			
		||||
  },
 | 
			
		||||
  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, StateInterface> = {
 | 
			
		||||
  getCurrentUser({ commit, rootState }) {
 | 
			
		||||
    if (rootState.session.currentSession) {
 | 
			
		||||
      commit('setLoading', { key: 'getUserLoading', data: true });
 | 
			
		||||
      commit('setLoading');
 | 
			
		||||
      axios
 | 
			
		||||
        .get(`/users/${rootState.session.currentSession.userid}`)
 | 
			
		||||
        .then((response: AxiosResponse<FG.User>) => {
 | 
			
		||||
        .then((response: AxiosResponse<CurrentUserResponse>) => {
 | 
			
		||||
          commit('setCurrentUser', response.data);
 | 
			
		||||
          commit('setCurrentPermissions', response.data.permissions);
 | 
			
		||||
        })
 | 
			
		||||
        .catch(err => {
 | 
			
		||||
          console.warn(err);
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          commit('setLoading', { key: 'getUserLoading', data: false });
 | 
			
		||||
          commit('setLoading', false);
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.debug('User not logged in, can not get current_user.');
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +77,7 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
 | 
			
		|||
 | 
			
		||||
  getUsers({ commit }) {
 | 
			
		||||
    axios
 | 
			
		||||
      .get(`/users`)
 | 
			
		||||
      .get('/users')
 | 
			
		||||
      .then((response: AxiosResponse<FG.User[]>) => {
 | 
			
		||||
        commit('setUsers', response.data);
 | 
			
		||||
      })
 | 
			
		||||
| 
						 | 
				
			
			@ -69,20 +86,43 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
 | 
			
		|||
      });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  updateUser({ commit, state, dispatch }, data) {
 | 
			
		||||
    commit('setLoading', { key: 'updateUserLoading', data: true });
 | 
			
		||||
    if (!state.currentUser) throw 'Not logged in';
 | 
			
		||||
  updateUser({ commit, state, dispatch }, data: FG.User) {
 | 
			
		||||
    commit('setLoading');
 | 
			
		||||
    axios
 | 
			
		||||
      .put(`/users/${state.currentUser.userid}`, data)
 | 
			
		||||
      .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', { key: 'updateUserLoading', data: false });
 | 
			
		||||
        commit('setLoading', false);
 | 
			
		||||
      });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  getRoles({ commit, state }, force = true) {
 | 
			
		||||
    if (!force && state.roles.length > 0) return;
 | 
			
		||||
    commit('setLoading');
 | 
			
		||||
    axios
 | 
			
		||||
      .get('/roles')
 | 
			
		||||
      .then((response: AxiosResponse<FG.Role[]>) => {
 | 
			
		||||
        commit('setRoles', response.data);
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => commit('setLoading', false));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  getPermissions({ commit, state }, force = true) {
 | 
			
		||||
    if (!force && state.permissions.length > 0) return;
 | 
			
		||||
    commit('setLoading');
 | 
			
		||||
    axios
 | 
			
		||||
      .get('/roles')
 | 
			
		||||
      .then((response: AxiosResponse<FG.Permission[]>) => {
 | 
			
		||||
        commit('setPermissions', response.data);
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => commit('setLoading', false));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -93,20 +133,11 @@ const getters: GetterTree<UserStateInterface, StateInterface> = {
 | 
			
		|||
  users({ users }) {
 | 
			
		||||
    return users;
 | 
			
		||||
  },
 | 
			
		||||
  loading({ updateUserLoading, getUserLoading }) {
 | 
			
		||||
    return updateUserLoading || getUserLoading;
 | 
			
		||||
  loading({ loading }) {
 | 
			
		||||
    return loading > 0;
 | 
			
		||||
  },
 | 
			
		||||
  displayName({ currentUser }) {
 | 
			
		||||
    return currentUser?.display_name;
 | 
			
		||||
  },
 | 
			
		||||
  permissions({ currentUser }) {
 | 
			
		||||
    let permissions: string[] = [];
 | 
			
		||||
    if (currentUser) {
 | 
			
		||||
      currentUser.roles.forEach(role => {
 | 
			
		||||
        permissions = permissions.concat(role.permissions);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return permissions;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue