Compare commits
	
		
			1 Commits
		
	
	
		
			develop
			...
			NativeStor
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						8ced3e6ce1 | 
							
								
								
									
										30
									
								
								.eslintrc.js
								
								
								
								
							
							
						
						
									
										30
									
								
								.eslintrc.js
								
								
								
								
							| 
						 | 
					@ -17,11 +17,11 @@ module.exports = {
 | 
				
			||||||
    project: resolve(__dirname, './tsconfig.json'),
 | 
					    project: resolve(__dirname, './tsconfig.json'),
 | 
				
			||||||
    tsconfigRootDir: __dirname,
 | 
					    tsconfigRootDir: __dirname,
 | 
				
			||||||
    ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
 | 
					    ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
 | 
				
			||||||
    sourceType: 'module', // Allows for the use of imports
 | 
					    sourceType: 'module' // Allows for the use of imports
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  env: {
 | 
					  env: {
 | 
				
			||||||
    browser: true,
 | 
					    browser: true
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Rules order is important, please avoid shuffling them
 | 
					  // Rules order is important, please avoid shuffling them
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ module.exports = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://github.com/prettier/eslint-config-prettier#installation
 | 
					    // https://github.com/prettier/eslint-config-prettier#installation
 | 
				
			||||||
    // usage with Prettier, provided by 'eslint-config-prettier'.
 | 
					    // usage with Prettier, provided by 'eslint-config-prettier'.
 | 
				
			||||||
    'plugin:prettier/recommended',
 | 
					    'prettier', //'plugin:prettier/recommended'
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
| 
						 | 
					@ -54,6 +54,10 @@ module.exports = {
 | 
				
			||||||
    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
 | 
					    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
 | 
				
			||||||
    // required to lint *.vue files
 | 
					    // required to lint *.vue files
 | 
				
			||||||
    'vue',
 | 
					    'vue',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
 | 
				
			||||||
 | 
					    // Prettier has not been included as plugin to avoid performance impact
 | 
				
			||||||
 | 
					    // add it as an extension for your IDE
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  globals: {
 | 
					  globals: {
 | 
				
			||||||
| 
						 | 
					@ -66,7 +70,7 @@ module.exports = {
 | 
				
			||||||
    __QUASAR_SSR_PWA__: true,
 | 
					    __QUASAR_SSR_PWA__: true,
 | 
				
			||||||
    process: true,
 | 
					    process: true,
 | 
				
			||||||
    Capacitor: true,
 | 
					    Capacitor: true,
 | 
				
			||||||
    chrome: true,
 | 
					    chrome: true
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // add your custom rules here
 | 
					  // add your custom rules here
 | 
				
			||||||
| 
						 | 
					@ -76,16 +80,20 @@ module.exports = {
 | 
				
			||||||
    'vue/multi-word-component-names': 'off',
 | 
					    'vue/multi-word-component-names': 'off',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Rejects on promises should always be of the Error type (and allow empty rejects as well)
 | 
					    // Rejects on promises should always be of the Error type (and allow empty rejects as well)
 | 
				
			||||||
    'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
 | 
					    'prefer-promise-reject-errors': ["error", {"allowEmptyReject": true}],
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Allow " if ' is contained inside the string, so we can avoid escaping
 | 
					 | 
				
			||||||
    quotes: ['error', 'single', { avoidEscape: true }],
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Allow " if ' is contained inside the string, so we can avoid escaping 
 | 
				
			||||||
 | 
					    quotes: [
 | 
				
			||||||
 | 
					      process.env.NODE_ENV === 'production' ? 'error' : 'warn',
 | 
				
			||||||
 | 
					      'single',
 | 
				
			||||||
 | 
					      { avoidEscape: true }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
    // TypeScript, let us be not too strict
 | 
					    // TypeScript, let us be not too strict
 | 
				
			||||||
    '@typescript-eslint/explicit-function-return-type': 'off',
 | 
					    '@typescript-eslint/explicit-function-return-type': 'off',
 | 
				
			||||||
    '@typescript-eslint/explicit-module-boundary-types': 'off',
 | 
					    '@typescript-eslint/explicit-module-boundary-types': 'off',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // allow debugger during development only
 | 
					    // allow debugger during development only
 | 
				
			||||||
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
 | 
					    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
 | 
				
			||||||
  },
 | 
					  }
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,8 +18,6 @@ yarn.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Capacitor related directories and files
 | 
					# Capacitor related directories and files
 | 
				
			||||||
/src-capacitor/www
 | 
					/src-capacitor/www
 | 
				
			||||||
/src-capacitor/android
 | 
					 | 
				
			||||||
/src-capacitor/ios
 | 
					 | 
				
			||||||
/src-capacitor/node_modules
 | 
					/src-capacitor/node_modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# BEX related directories and files
 | 
					# BEX related directories and files
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,6 @@
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
    // to edit target browsers: use "browserslist" field in package.json
 | 
					    // to edit target browsers: use "browserslist" field in package.json
 | 
				
			||||||
    require('autoprefixer'),
 | 
					    require('autoprefixer')
 | 
				
			||||||
  ],
 | 
					  ]
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
pipeline:
 | 
					 | 
				
			||||||
  install:
 | 
					 | 
				
			||||||
    image: node:lts-alpine
 | 
					 | 
				
			||||||
    commands:
 | 
					 | 
				
			||||||
      - yarn install
 | 
					 | 
				
			||||||
  lint:
 | 
					 | 
				
			||||||
    image: node:lts-alpine
 | 
					 | 
				
			||||||
    commands:
 | 
					 | 
				
			||||||
      - yarn lint
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  build:
 | 
					 | 
				
			||||||
    image: node:lts-alpine
 | 
					 | 
				
			||||||
    commands:
 | 
					 | 
				
			||||||
      - yarn quasar build
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
branches: [main, develop]
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,5 @@
 | 
				
			||||||
# Flaschengeist (frontend)
 | 
					# Flaschengeist (frontend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Modular student club administration system, licensed under the MIT license.
 | 
					Modular student club administration system, licensed under the MIT license.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,4 +44,4 @@ export default defineComponent({
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -1,46 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-avatar>
 | 
					 | 
				
			||||||
    <slot :avatarURL="avatarURL(modelValue)">
 | 
					 | 
				
			||||||
      <q-img :src="avatarURL(modelValue)" style="min-width: 100%; min-height: 100%">
 | 
					 | 
				
			||||||
        <template #error>
 | 
					 | 
				
			||||||
          <img :src="fallback" style="height: 100%" />
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </q-img>
 | 
					 | 
				
			||||||
    </slot>
 | 
					 | 
				
			||||||
  </q-avatar>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { PropType, defineComponent } from 'vue';
 | 
					 | 
				
			||||||
import { avatarURL } from '@flaschengeist/api';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Display an avatar for an user
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Slots:
 | 
					 | 
				
			||||||
 *  default - scope: {avatarURL}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'UserAvatar',
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      type: [Object, String] as PropType<FG.User | string>,
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    showZoom: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    fallback: {
 | 
					 | 
				
			||||||
      type: String,
 | 
					 | 
				
			||||||
      default: 'no-image.svg',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: ['error'],
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      avatarURL,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,4 @@
 | 
				
			||||||
import IsoDateInput from './IsoDateInput.vue';
 | 
					import IsoDateInput from './IsoDateInput.vue';
 | 
				
			||||||
import PasswordInput from './PasswordInput.vue';
 | 
					import PasswordInput from './PasswordInput.vue';
 | 
				
			||||||
import UserAvatar from './UserAvatar.vue';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { IsoDateInput, PasswordInput, UserAvatar };
 | 
					export { IsoDateInput, PasswordInput };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,5 @@ export * from './src/stores/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export * from './src/utils/datetime';
 | 
					export * from './src/utils/datetime';
 | 
				
			||||||
export * from './src/utils/permission';
 | 
					export * from './src/utils/permission';
 | 
				
			||||||
export * from './src/utils/persistent';
 | 
					 | 
				
			||||||
export * from './src/utils/user';
 | 
					 | 
				
			||||||
export * from './src/utils/validators';
 | 
					export * from './src/utils/validators';
 | 
				
			||||||
export * from './src/utils/misc';
 | 
					export * from './src/utils/misc';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
  "version": "1.0.0-alpha.7",
 | 
					  "version": "1.0.0-alpha.4",
 | 
				
			||||||
  "name": "@flaschengeist/api",
 | 
					  "name": "@flaschengeist/api",
 | 
				
			||||||
  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
					  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
				
			||||||
| 
						 | 
					@ -8,16 +8,27 @@
 | 
				
			||||||
  "bugs": {
 | 
					  "bugs": {
 | 
				
			||||||
    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
				
			||||||
 | 
					    "lint": "eslint --ext .js,.ts,.vue ./src"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "main": "./src/index.ts",
 | 
					  "main": "./src/index.ts",
 | 
				
			||||||
  "peerDependencies": {
 | 
					  "peerDependencies": {
 | 
				
			||||||
    "@quasar/app": "^3.2.4",
 | 
					    "@quasar/app": "^3.2.3",
 | 
				
			||||||
    "flaschengeist": "^2.0.0-alpha.1",
 | 
					    "flaschengeist": "^2.0.0-alpha.1",
 | 
				
			||||||
    "pinia": "^2.0.6"
 | 
					    "pinia": "^2.0.4"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@flaschengeist/types": "^1.0.0-alpha.10",
 | 
					    "@flaschengeist/types": "^1.0.0-alpha.5",
 | 
				
			||||||
    "@types/node": "^14.18.00",
 | 
					    "@types/node": "^12.20.37",
 | 
				
			||||||
    "typescript": "^4.5.2"
 | 
					    "@typescript-eslint/eslint-plugin": "^5.4.0",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^5.4.0",
 | 
				
			||||||
 | 
					    "eslint": "^8.3.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^8.3.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-vue": "^8.1.1",
 | 
				
			||||||
 | 
					    "eslint-webpack-plugin": "^3.1.1",
 | 
				
			||||||
 | 
					    "prettier": "^2.4.1",
 | 
				
			||||||
 | 
					    "typescript": "^4.4.4"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "prettier": {
 | 
					  "prettier": {
 | 
				
			||||||
    "singleQuote": true,
 | 
					    "singleQuote": true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,9 @@
 | 
				
			||||||
 | 
					import { SessionStorage } from 'quasar';
 | 
				
			||||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import { fixSession, useSessionStore, useUserStore } from '.';
 | 
					import { useSessionStore, useUserStore } from '.';
 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
import { api } from '../internal';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
import { PersistentStorage } from '../utils/persistent';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function reviveSession() {
 | 
					 | 
				
			||||||
  return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function clearPersistant() {
 | 
					 | 
				
			||||||
  void PersistentStorage.remove('fg_session');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function saveSession(session?: FG.Session) {
 | 
					 | 
				
			||||||
  if (session === undefined) return clearPersistant();
 | 
					 | 
				
			||||||
  PersistentStorage.set('fg_session', session).catch(() =>
 | 
					 | 
				
			||||||
    console.error('Could not save token to storage')
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useMainStore = defineStore({
 | 
					export const useMainStore = defineStore({
 | 
				
			||||||
  id: 'main',
 | 
					  id: 'main',
 | 
				
			||||||
| 
						 | 
					@ -38,10 +23,6 @@ export const useMainStore = defineStore({
 | 
				
			||||||
      if (this.user === undefined) throw 'Not logged in, this should not be called';
 | 
					      if (this.user === undefined) throw 'Not logged in, this should not be called';
 | 
				
			||||||
      return this.user;
 | 
					      return this.user;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    currentSession(): FG.Session {
 | 
					 | 
				
			||||||
      if (this.session === undefined) throw 'Not logged in, this should not be called';
 | 
					 | 
				
			||||||
      return this.session;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    permissions(): string[] {
 | 
					    permissions(): string[] {
 | 
				
			||||||
      return this.user?.permissions || [];
 | 
					      return this.user?.permissions || [];
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -52,29 +33,28 @@ export const useMainStore = defineStore({
 | 
				
			||||||
     *  Updates session and loads current user
 | 
					     *  Updates session and loads current user
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async init() {
 | 
					    async init() {
 | 
				
			||||||
      const sessionStore = useSessionStore();
 | 
					      if (this.session) {
 | 
				
			||||||
      const userStore = useUserStore();
 | 
					        const sessionStore = useSessionStore();
 | 
				
			||||||
 | 
					        const session = await sessionStore.getSession(this.session.token);
 | 
				
			||||||
      try {
 | 
					        if (session) {
 | 
				
			||||||
        this.session = await reviveSession();
 | 
					          this.session = session;
 | 
				
			||||||
        if (this.session !== undefined) {
 | 
					          const userStore = useUserStore();
 | 
				
			||||||
          this.session = await sessionStore.getSession(this.session.token);
 | 
					          const user = await userStore.getUser(this.session.userid);
 | 
				
			||||||
          if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
 | 
					          if (user) {
 | 
				
			||||||
 | 
					            this.user = user;
 | 
				
			||||||
 | 
					            SessionStorage.set('user', user);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.warn('Could not load token from storage', error);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async login(userid: string, password: string) {
 | 
					    async login(userid: string, password: string) {
 | 
				
			||||||
      const userStore = useUserStore();
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { data } = await api.post<FG.Session>('/auth', { userid, password });
 | 
					        const { data } = await api.post<FG.Session>('/auth', { userid, password });
 | 
				
			||||||
        this.session = fixSession(data);
 | 
					        this.session = data;
 | 
				
			||||||
        this.user = await userStore.getUser(data.userid, true);
 | 
					        this.session.expires = new Date(this.session.expires);
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      } catch ({ response }) {
 | 
					      } catch ({ response }) {
 | 
				
			||||||
        this.handleLoggedOut();
 | 
					 | 
				
			||||||
        return (<AxiosResponse | undefined>response)?.status || false;
 | 
					        return (<AxiosResponse | undefined>response)?.status || false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -150,10 +130,14 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    async setShortcuts() {
 | 
					    async setShortcuts() {
 | 
				
			||||||
      await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
 | 
					      await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					 | 
				
			||||||
    handleLoggedOut() {
 | 
					    handleLoggedOut() {
 | 
				
			||||||
      this.$reset();
 | 
					      this.$patch({
 | 
				
			||||||
      void clearPersistant();
 | 
					        session: undefined,
 | 
				
			||||||
 | 
					        user: undefined,
 | 
				
			||||||
 | 
					        notifications: [],
 | 
				
			||||||
 | 
					        shortcuts: [],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      SessionStorage.clear();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,6 @@ import { defineStore } from 'pinia';
 | 
				
			||||||
import { api } from '../internal';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { isAxiosError, useMainStore } from '.';
 | 
					import { isAxiosError, useMainStore } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fixSession(s?: FG.Session) {
 | 
					 | 
				
			||||||
  return !s ? s : Object.assign(s, { expires: new Date(s.expires) });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useSessionStore = defineStore({
 | 
					export const useSessionStore = defineStore({
 | 
				
			||||||
  id: 'sessions',
 | 
					  id: 'sessions',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,13 +15,15 @@ export const useSessionStore = defineStore({
 | 
				
			||||||
      return await api
 | 
					      return await api
 | 
				
			||||||
        .get(`/auth/${token}`)
 | 
					        .get(`/auth/${token}`)
 | 
				
			||||||
        .then(({ data }: AxiosResponse<FG.Session>) => data)
 | 
					        .then(({ data }: AxiosResponse<FG.Session>) => data)
 | 
				
			||||||
        .catch(() => undefined);
 | 
					        .catch(() => null);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getSessions() {
 | 
					    async getSessions() {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { data } = await api.get<FG.Session[]>('/auth');
 | 
					        const { data } = await api.get<FG.Session[]>('/auth');
 | 
				
			||||||
        data.forEach(fixSession);
 | 
					        data.forEach((session) => {
 | 
				
			||||||
 | 
					          session.expires = new Date(session.expires);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const mainStore = useMainStore();
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
        const currentSession = data.find((session) => {
 | 
					        const currentSession = data.find((session) => {
 | 
				
			||||||
| 
						 | 
					@ -57,7 +55,7 @@ export const useSessionStore = defineStore({
 | 
				
			||||||
    async updateSession(lifetime: number, token: string) {
 | 
					    async updateSession(lifetime: number, token: string) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
					        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
				
			||||||
        fixSession(data);
 | 
					        data.expires = new Date(data.expires);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const mainStore = useMainStore();
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
					        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,157 +2,81 @@ import { defineStore } from 'pinia';
 | 
				
			||||||
import { api } from '../internal';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { isAxiosError, useMainStore } from '.';
 | 
					import { isAxiosError, useMainStore } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fixUser(u?: FG.User) {
 | 
					 | 
				
			||||||
  return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Check if state is outdated / dirty
 | 
					 | 
				
			||||||
 * Value is considered outdated after 15 minutes
 | 
					 | 
				
			||||||
 * @param updated Time of last updated (in milliseconds see Date.now())
 | 
					 | 
				
			||||||
 * @returns True if outdated, false otherwise
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function isDirty(updated: number) {
 | 
					 | 
				
			||||||
  return Date.now() - updated > 15 * 60 * 1000;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useUserStore = defineStore({
 | 
					export const useUserStore = defineStore({
 | 
				
			||||||
  id: 'users',
 | 
					  id: 'users',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state: () => ({
 | 
					  state: () => ({
 | 
				
			||||||
    roles: [] as FG.Role[],
 | 
					    roles: [] as FG.Role[],
 | 
				
			||||||
 | 
					    users: [] as FG.User[],
 | 
				
			||||||
    permissions: [] as FG.Permission[],
 | 
					    permissions: [] as FG.Permission[],
 | 
				
			||||||
    // list of all users, include deleted ones, use `users` getter for list of active ones
 | 
					    _dirty_users: true,
 | 
				
			||||||
    _users: [] as FG.User[],
 | 
					    _dirty_roles: true,
 | 
				
			||||||
    // Internal flags for deciding if lists need to force-loaded
 | 
					 | 
				
			||||||
    _dirty_users: 0,
 | 
					 | 
				
			||||||
    _dirty_roles: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getters: {
 | 
					  getters: {},
 | 
				
			||||||
    users(state) {
 | 
					 | 
				
			||||||
      return state._users.filter((u) => !u.deleted);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  actions: {
 | 
					  actions: {
 | 
				
			||||||
    /** Simply filter all users by ID */
 | 
					 | 
				
			||||||
    findUser(userid: string) {
 | 
					    findUser(userid: string) {
 | 
				
			||||||
      return this._users.find((user) => user.userid === userid);
 | 
					      return this.users.find((user) => user.userid === userid);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Retrieve user by ID
 | 
					 | 
				
			||||||
     * @param userid ID of user to retrieve
 | 
					 | 
				
			||||||
     * @param force If set to true the user is loaded from backend even when a local copy is available
 | 
					 | 
				
			||||||
     * @returns Retrieved user (Promise) or raise an error
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if loading failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async getUser(userid: string, force = false) {
 | 
					    async getUser(userid: string, force = false) {
 | 
				
			||||||
      const idx = this._users.findIndex((user) => user.userid === userid);
 | 
					      const idx = this.users.findIndex((user) => user.userid === userid);
 | 
				
			||||||
      if (force || idx === -1 || isDirty(this._dirty_users)) {
 | 
					      if (force || this._dirty_users || idx === -1) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const { data } = await api.get<FG.User>(`/users/${userid}`);
 | 
					          const { data } = await api.get<FG.User>(`/users/${userid}`);
 | 
				
			||||||
          fixUser(data);
 | 
					          if (data.birthday) data.birthday = new Date(data.birthday);
 | 
				
			||||||
          if (idx === -1) this._users.push(data);
 | 
					          if (idx === -1) this.users.push(data);
 | 
				
			||||||
          else this._users[idx] = data;
 | 
					          else this.users[idx] = data;
 | 
				
			||||||
          return data;
 | 
					          return data;
 | 
				
			||||||
        } catch (error) {
 | 
					        } catch (error) {
 | 
				
			||||||
          // Ignore 404, throw all other
 | 
					          // Ignore 404, throw all other
 | 
				
			||||||
          if (!isAxiosError(error, 404)) throw error;
 | 
					          if (!isAxiosError(error, 404)) throw error;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        return this._users[idx];
 | 
					        return this.users[idx];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Retrieve list of all users
 | 
					 | 
				
			||||||
     * @param force If set to true a fresh users list is loaded from backend even when a local copy is available
 | 
					 | 
				
			||||||
     * @returns Array of retrieved users (Promise)
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if loading failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async getUsers(force = false) {
 | 
					    async getUsers(force = false) {
 | 
				
			||||||
      if (force || isDirty(this._dirty_users)) {
 | 
					      if (force || this._dirty_users) {
 | 
				
			||||||
        const { data } = await api.get<FG.User[]>('/users');
 | 
					        const { data } = await api.get<FG.User[]>('/users');
 | 
				
			||||||
        data.forEach(fixUser);
 | 
					        data.forEach((user) => {
 | 
				
			||||||
        this._users = data;
 | 
					          if (user.birthday) user.birthday = new Date(user.birthday);
 | 
				
			||||||
        this._dirty_users = Date.now();
 | 
					        });
 | 
				
			||||||
 | 
					        this.users = data;
 | 
				
			||||||
 | 
					        this._dirty_users = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return this._users;
 | 
					      return this.users;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Save modifications of user on backend
 | 
					 | 
				
			||||||
     * @param user Modified user to save
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async updateUser(user: FG.User) {
 | 
					    async updateUser(user: FG.User) {
 | 
				
			||||||
      await api.put(`/users/${user.userid}`, user);
 | 
					      await api.put(`/users/${user.userid}`, user);
 | 
				
			||||||
      // Modifcation accepted by backend
 | 
					
 | 
				
			||||||
      // Save modifications back to our users list
 | 
					 | 
				
			||||||
      const idx = this._users.findIndex((u) => u.userid === user.userid);
 | 
					 | 
				
			||||||
      if (idx > -1) this._users[idx] = user;
 | 
					 | 
				
			||||||
      // If user was current user, save modifications back to the main store
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					      const mainStore = useMainStore();
 | 
				
			||||||
      if (user.userid === mainStore.user?.userid) mainStore.user = user;
 | 
					      if (user.userid === mainStore.user?.userid) mainStore.user = user;
 | 
				
			||||||
 | 
					      this._dirty_users = true;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Register a new user
 | 
					 | 
				
			||||||
     * @param user User to register (id not set)
 | 
					 | 
				
			||||||
     * @returns The registered user (id set)
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async createUser(user: FG.User) {
 | 
					    async createUser(user: FG.User) {
 | 
				
			||||||
      const { data } = await api.post<FG.User>('/users', user);
 | 
					      const { data } = await api.post<FG.User>('/users', user);
 | 
				
			||||||
      this._users.push(<FG.User>fixUser(data));
 | 
					      this.users.push(data);
 | 
				
			||||||
      return data;
 | 
					      return data;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Delete an user
 | 
					    async uploadAvatar(user: FG.User, file: string | File) {
 | 
				
			||||||
     * Throws if failed and resolves void if succeed
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param user User or ID of user to delete
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async deleteUser(user: FG.User | string) {
 | 
					 | 
				
			||||||
      if (typeof user === 'object') user = user.userid;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await api.delete(`/users/${user}`);
 | 
					 | 
				
			||||||
      this._users = this._users.filter((u) => u.userid != user);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Upload an avatar for an user
 | 
					 | 
				
			||||||
     * Throws if failed and resolves void if succeed
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param user User or ID of user
 | 
					 | 
				
			||||||
     * @param file Avatar file to upload
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async uploadAvatar(user: FG.User | string, file: string | File) {
 | 
					 | 
				
			||||||
      if (typeof user === 'object') user = user.userid;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const formData = new FormData();
 | 
					      const formData = new FormData();
 | 
				
			||||||
      formData.append('file', file);
 | 
					      formData.append('file', file);
 | 
				
			||||||
      await api.post(`/users/${user}/avatar`, formData, {
 | 
					      await api.post(`/users/${user.userid}/avatar`, formData, {
 | 
				
			||||||
        headers: {
 | 
					        headers: {
 | 
				
			||||||
          'Content-Type': 'multipart/form-data',
 | 
					          'Content-Type': 'multipart/form-data',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Delete avatar of an user
 | 
					    async deleteAvatar(user: FG.User) {
 | 
				
			||||||
     * @param user User or ID of user
 | 
					      await api.delete(`/users/${user.userid}/avatar`)
 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async deleteAvatar(user: FG.User | string) {
 | 
					 | 
				
			||||||
      if (typeof user === 'object') user = user.userid;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await api.delete(`/users/${user}/avatar`);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Retrieve list of all permissions
 | 
					 | 
				
			||||||
     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
					 | 
				
			||||||
     * @returns Array of retrieved permissions (Promise)
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async getPermissions(force = false) {
 | 
					    async getPermissions(force = false) {
 | 
				
			||||||
      if (force || this.permissions.length === 0) {
 | 
					      if (force || this.permissions.length === 0) {
 | 
				
			||||||
        const { data } = await api.get<FG.Permission[]>('/roles/permissions');
 | 
					        const { data } = await api.get<FG.Permission[]>('/roles/permissions');
 | 
				
			||||||
| 
						 | 
					@ -161,51 +85,39 @@ export const useUserStore = defineStore({
 | 
				
			||||||
      return this.permissions;
 | 
					      return this.permissions;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Retrieve list of all roles
 | 
					 | 
				
			||||||
     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
					 | 
				
			||||||
     * @returns Array of retrieved roles (Promise)
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async getRoles(force = false) {
 | 
					    async getRoles(force = false) {
 | 
				
			||||||
      if (force || isDirty(this._dirty_roles)) {
 | 
					      if (force || this._dirty_roles) {
 | 
				
			||||||
        const { data } = await api.get<FG.Role[]>('/roles');
 | 
					        const { data } = await api.get<FG.Role[]>('/roles');
 | 
				
			||||||
        this.roles = data;
 | 
					        this.roles = data;
 | 
				
			||||||
        this._dirty_roles = Date.now();
 | 
					        this._dirty_roles = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return this.roles;
 | 
					      return this.roles;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Save modifications of role on the backend
 | 
					 | 
				
			||||||
     * @param role role to save
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async updateRole(role: FG.Role) {
 | 
					    async updateRole(role: FG.Role) {
 | 
				
			||||||
      await api.put(`/roles/${role.id}`, role);
 | 
					      try {
 | 
				
			||||||
 | 
					        await api.put(`/roles/${role.id}`, role);
 | 
				
			||||||
      const idx = this.roles.findIndex((r) => r.id === role.id);
 | 
					      } catch (error) {
 | 
				
			||||||
      if (idx != -1) this.roles[idx] = role;
 | 
					        console.warn(error);
 | 
				
			||||||
      else this._dirty_roles = 0;
 | 
					      }
 | 
				
			||||||
 | 
					      this._updatePermission(role);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _updatePermission(role: FG.Role) {
 | 
				
			||||||
 | 
					      const idx = this.roles.findIndex((r) => r.id === role.id);
 | 
				
			||||||
 | 
					      if (idx != -1) this.roles[idx] = role;
 | 
				
			||||||
 | 
					      this._dirty_roles = true;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Create a new role
 | 
					 | 
				
			||||||
     * @param role Role to create (ID not set)
 | 
					 | 
				
			||||||
     * @returns Created role (ID set)
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async newRole(role: FG.Role) {
 | 
					    async newRole(role: FG.Role) {
 | 
				
			||||||
      const { data } = await api.post<FG.Role>('/roles', role);
 | 
					      const { data } = await api.post<FG.Role>('/roles', role);
 | 
				
			||||||
      this.roles.push(data);
 | 
					      this.roles.push(data);
 | 
				
			||||||
      return data;
 | 
					      return data;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Delete a role
 | 
					 | 
				
			||||||
     * @param role Role or ID of role to delete
 | 
					 | 
				
			||||||
     * @throws Probably an AxiosError if request failed (409 if role still in use)
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async deleteRole(role: FG.Role | number) {
 | 
					    async deleteRole(role: FG.Role | number) {
 | 
				
			||||||
      if (typeof role === 'object') role = role.id;
 | 
					      await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`);
 | 
				
			||||||
      await api.delete(`/roles/${role}`);
 | 
					      this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id));
 | 
				
			||||||
      this.roles = this.roles.filter((r) => r.id !== role);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,12 +17,12 @@ export function formatDateTime(
 | 
				
			||||||
  return dateTimeFormat.format(date);
 | 
					  return dateTimeFormat.format(date);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function asDate(date?: Date, placeholder = '') {
 | 
					export function asDate(date?: Date) {
 | 
				
			||||||
  return date ? formatDateTime(date, true) : placeholder;
 | 
					  return date ? formatDateTime(date, true) : '';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function asHour(date?: Date, placeholder = '') {
 | 
					export function asHour(date?: Date) {
 | 
				
			||||||
  return date ? formatDateTime(date, false, true) : placeholder;
 | 
					  return date ? formatDateTime(date, false, true) : '';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function formatStartEnd(start: Date, end?: Date) {
 | 
					export function formatStartEnd(start: Date, end?: Date) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,35 +0,0 @@
 | 
				
			||||||
import { LocalStorage, Platform } from 'quasar';
 | 
					 | 
				
			||||||
import { Storage } from '@capacitor/storage';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
type PersitentTypes = Date | RegExp | number | boolean | string | object;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class PersistentStorage {
 | 
					 | 
				
			||||||
  static clear() {
 | 
					 | 
				
			||||||
    if (Platform.is.capacitor) return Storage.clear();
 | 
					 | 
				
			||||||
    else return Promise.resolve(LocalStorage.clear());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static remove(key: string) {
 | 
					 | 
				
			||||||
    if (Platform.is.capacitor) return Storage.remove({ key: key });
 | 
					 | 
				
			||||||
    else return Promise.resolve(LocalStorage.remove(key));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static set(key: string, value: PersitentTypes) {
 | 
					 | 
				
			||||||
    if (Platform.is.capacitor) return Storage.set({ key, value: JSON.stringify(value) });
 | 
					 | 
				
			||||||
    else return Promise.resolve(LocalStorage.set(key, value));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static get<T extends PersitentTypes>(key: string) {
 | 
					 | 
				
			||||||
    if (Platform.is.capacitor)
 | 
					 | 
				
			||||||
      return Storage.get({ key }).then((v) =>
 | 
					 | 
				
			||||||
        v.value === null ? null : (JSON.parse(v.value) as T)
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    else return Promise.resolve(LocalStorage.getItem<T>(key));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static keys() {
 | 
					 | 
				
			||||||
    if (Platform.is.capacitor) return Storage.keys().then((v) => v.keys);
 | 
					 | 
				
			||||||
    else return Promise.resolve(LocalStorage.getAllKeys());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +0,0 @@
 | 
				
			||||||
import { api } from '../internal';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function avatarURL(user: FG.User | string, thumbnail = true) {
 | 
					 | 
				
			||||||
  if (typeof user === 'object') user = user.userid;
 | 
					 | 
				
			||||||
  return `${api.defaults?.baseURL || ''}/users/${user}/avatar${thumbnail ? '?thumbnail' : ''}`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,6 @@
 | 
				
			||||||
/* eslint-env node */
 | 
					/* eslint-env node */
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  presets: ['@quasar/babel-preset-app'],
 | 
					  presets: [
 | 
				
			||||||
};
 | 
					    '@quasar/babel-preset-app'
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								package.json
								
								
								
								
							
							
						
						
									
										35
									
								
								package.json
								
								
								
								
							| 
						 | 
					@ -11,34 +11,32 @@
 | 
				
			||||||
    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "format": "prettier --config ./package.json  --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
 | 
					    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
				
			||||||
    "lint": "eslint --ext .js,.ts,.vue ./src ./api"
 | 
					    "lint": "eslint --ext .js,.ts,.vue ./src ./api"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@flaschengeist/api": "file:./api",
 | 
					    "@flaschengeist/api": "file:./api",
 | 
				
			||||||
    "@flaschengeist/users": "^1.0.0-alpha.3",
 | 
					    "@flaschengeist/users": "^1.0.0-alpha.1",
 | 
				
			||||||
    "axios": "^0.24.0",
 | 
					    "axios": "^0.24.0",
 | 
				
			||||||
    "pinia": "^2.0.6",
 | 
					    "cordova": "^10.0.0",
 | 
				
			||||||
 | 
					    "pinia": "^2.0.4",
 | 
				
			||||||
    "quasar": "^2.3.3"
 | 
					    "quasar": "^2.3.3"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@capacitor/core": "^3.3.2",
 | 
					    "@flaschengeist/types": "^1.0.0-alpha.6",
 | 
				
			||||||
    "@capacitor/storage": "^1.2.3",
 | 
					    "@quasar/app": "^3.2.3",
 | 
				
			||||||
    "@flaschengeist/types": "^1.0.0-alpha.10",
 | 
					    "@quasar/extras": "^1.12.1",
 | 
				
			||||||
    "@quasar/app": "^3.2.4",
 | 
					    "@types/node": "^14.17.34",
 | 
				
			||||||
    "@quasar/extras": "^1.12.2",
 | 
					 | 
				
			||||||
    "@types/node": "^14.18.0",
 | 
					 | 
				
			||||||
    "@types/webpack": "^5.28.0",
 | 
					    "@types/webpack": "^5.28.0",
 | 
				
			||||||
    "@types/webpack-env": "^1.16.3",
 | 
					    "@types/webpack-env": "^1.16.3",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^5.5.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^5.4.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^5.5.0",
 | 
					    "@typescript-eslint/parser": "^5.4.0",
 | 
				
			||||||
    "eslint": "^8.4.0",
 | 
					    "eslint": "^8.3.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.3.0",
 | 
					    "eslint-config-prettier": "^8.3.0",
 | 
				
			||||||
    "eslint-plugin-prettier": "^4.0.0",
 | 
					 | 
				
			||||||
    "eslint-plugin-vue": "^8.1.1",
 | 
					    "eslint-plugin-vue": "^8.1.1",
 | 
				
			||||||
    "eslint-webpack-plugin": "^3.1.1",
 | 
					    "eslint-webpack-plugin": "^3.1.1",
 | 
				
			||||||
    "modify-source-webpack-plugin": "^3.0.0",
 | 
					    "modify-source-webpack-plugin": "^3.0.0",
 | 
				
			||||||
    "prettier": "^2.5.1",
 | 
					    "prettier": "^2.4.1",
 | 
				
			||||||
    "typescript": "^4.5.2",
 | 
					    "typescript": "^4.5.2",
 | 
				
			||||||
    "vuedraggable": "^4.1.0"
 | 
					    "vuedraggable": "^4.1.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -53,15 +51,14 @@
 | 
				
			||||||
      "Firefox esr",
 | 
					      "Firefox esr",
 | 
				
			||||||
      "last 6 Chrome versions",
 | 
					      "last 6 Chrome versions",
 | 
				
			||||||
      "last 4 Firefox versions",
 | 
					      "last 4 Firefox versions",
 | 
				
			||||||
      "last 4 Edge versions",
 | 
					    "last 4 Edge versions",
 | 
				
			||||||
      "last 4 Safari versions",
 | 
					    "last 4 Safari versions",
 | 
				
			||||||
      "last 4 ChromeAndroid versions",
 | 
					      "last 4 ChromeAndroid versions",
 | 
				
			||||||
      "last 1 FirefoxAndroid versions"
 | 
					      "last 1 FirefoxAndroid versions"
 | 
				
			||||||
    ],
 | 
					  ],
 | 
				
			||||||
    "cordova": [
 | 
					    "cordova": [
 | 
				
			||||||
      "iOS >= 13.0",
 | 
					      "iOS >= 13.0",
 | 
				
			||||||
      "Android >= 76",
 | 
					      "Android >= 76"
 | 
				
			||||||
      "ChromeAndroid >= 76"
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										102
									
								
								quasar.conf.js
								
								
								
								
							
							
						
						
									
										102
									
								
								quasar.conf.js
								
								
								
								
							| 
						 | 
					@ -8,9 +8,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* eslint-env node */
 | 
					/* eslint-env node */
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-var-requires */
 | 
					/* eslint-disable @typescript-eslint/no-var-requires */
 | 
				
			||||||
const ESLintPlugin = require('eslint-webpack-plugin');
 | 
					const ESLintPlugin = require('eslint-webpack-plugin')
 | 
				
			||||||
const { ModifySourcePlugin } = require('modify-source-webpack-plugin');
 | 
					const { ModifySourcePlugin } = require('modify-source-webpack-plugin')
 | 
				
			||||||
const { configure } = require('quasar/wrappers');
 | 
					const { configure } = require('quasar/wrappers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = configure(function (/* ctx */) {
 | 
					module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
| 
						 | 
					@ -21,7 +22,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
          enabled: true,
 | 
					          enabled: true,
 | 
				
			||||||
          files: './src/**/*.{ts,tsx,js,jsx,vue}',
 | 
					          files: './src/**/*.{ts,tsx,js,jsx,vue}',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/prefetch-feature
 | 
					    // https://quasar.dev/quasar-cli/prefetch-feature
 | 
				
			||||||
| 
						 | 
					@ -30,7 +31,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', 'store', 'plugins', 'login', 'init'],
 | 
					    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'],
 | 
				
			||||||
| 
						 | 
					@ -44,7 +45,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      // 'material-icons',
 | 
					      // 'material-icons',
 | 
				
			||||||
      'mdi-v6',
 | 
					      'mdi-v6',
 | 
				
			||||||
      // 'themify',
 | 
					      // 'themify',
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 | 
					      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 | 
				
			||||||
      'roboto-font', // optional, you are not bound to it
 | 
					      'roboto-font', // optional, you are not bound to it
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
| 
						 | 
					@ -55,6 +56,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // transpile: false,
 | 
					      // transpile: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
					      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
				
			||||||
      // (from node_modules, which are by default not transpiled).
 | 
					      // (from node_modules, which are by default not transpiled).
 | 
				
			||||||
      // Applies only if "transpile" is set to true.
 | 
					      // Applies only if "transpile" is set to true.
 | 
				
			||||||
| 
						 | 
					@ -69,38 +71,33 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // https://quasar.dev/quasar-cli/handling-webpack
 | 
					      // https://quasar.dev/quasar-cli/handling-webpack
 | 
				
			||||||
      // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
 | 
					      // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
 | 
				
			||||||
      chainWebpack(chain) {
 | 
					      chainWebpack (chain) {
 | 
				
			||||||
        chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
 | 
					        chain.plugin('eslint-webpack-plugin')
 | 
				
			||||||
          {
 | 
					          .use(ESLintPlugin, [{
 | 
				
			||||||
            extensions: ['ts', 'js', 'vue'],
 | 
					            extensions: [ 'ts', 'js', 'vue' ],
 | 
				
			||||||
            exclude: ['node_modules', 'src-capacitor'],
 | 
					            exclude: 'node_modules'
 | 
				
			||||||
          },
 | 
					          }])
 | 
				
			||||||
        ]);
 | 
					        chain.plugin('modify-source-webpack-plugin')
 | 
				
			||||||
        chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
 | 
					          .use(ModifySourcePlugin, [{
 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            rules: [
 | 
					            rules: [
 | 
				
			||||||
              {
 | 
					              {
 | 
				
			||||||
                test: /plugins\.ts$/,
 | 
					                test: /plugins\.ts$/,
 | 
				
			||||||
                modify: (src, filename) => {
 | 
					                modify: (src, filename) => {
 | 
				
			||||||
                  const custom_plgns = require('./plugin.config.js');
 | 
					                  const custom_plgns = require('./plugin.config.js')
 | 
				
			||||||
                  const required_plgns = require('./src/vendor-plugin.config.js');
 | 
					                  const required_plgns = require('./src/vendor-plugin.config.js')
 | 
				
			||||||
                  return src.replace(
 | 
					                  return src.replace(/\/\* *INSERT_PLUGIN_LIST *\*\//, 
 | 
				
			||||||
                    /\/\* *INSERT_PLUGIN_LIST *\*\//,
 | 
					                  [...custom_plgns, ...required_plgns].map(v => `import("${v}").catch(() => "${v}")`)
 | 
				
			||||||
                    [...custom_plgns, ...required_plgns]
 | 
					                    .join(','))
 | 
				
			||||||
                      .map((v) => `import("${v}").catch(() => "${v}")`)
 | 
					                  }
 | 
				
			||||||
                      .join(',')
 | 
					              }
 | 
				
			||||||
                  );
 | 
					            ]
 | 
				
			||||||
                },
 | 
					          }])
 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
        chain.merge({
 | 
					        chain.merge({
 | 
				
			||||||
          snapshot: {
 | 
					          snapshot: {
 | 
				
			||||||
            managedPaths: [],
 | 
					            managedPaths: []
 | 
				
			||||||
          },
 | 
					          }
 | 
				
			||||||
        });
 | 
					        })
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
 | 
					    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
 | 
				
			||||||
| 
						 | 
					@ -108,7 +105,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      https: false,
 | 
					      https: false,
 | 
				
			||||||
      port: 8080,
 | 
					      port: 8080,
 | 
				
			||||||
      open: false, // opens browser window automatically
 | 
					      open: false, // opens browser window automatically
 | 
				
			||||||
      watchFiles: { paths: ['/node_modules/@flaschengeist/**/*'] },
 | 
					      watchFiles: {paths: ['/node_modules/@flaschengeist/**/*']}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
 | 
					    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
 | 
				
			||||||
| 
						 | 
					@ -120,8 +117,8 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
        loadingBar: {
 | 
					        loadingBar: {
 | 
				
			||||||
          position: 'top',
 | 
					          position: 'top',
 | 
				
			||||||
          color: 'warning',
 | 
					          color: 'warning',
 | 
				
			||||||
          size: '5px',
 | 
					          size: '5px'
 | 
				
			||||||
        },
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // For special cases outside of where the auto-import stategy can have an impact
 | 
					      // For special cases outside of where the auto-import stategy can have an impact
 | 
				
			||||||
| 
						 | 
					@ -132,7 +129,14 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      // directives: [],
 | 
					      // directives: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Quasar plugins
 | 
					      // Quasar plugins
 | 
				
			||||||
      plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
 | 
					      plugins: [
 | 
				
			||||||
 | 
					        'LocalStorage',
 | 
				
			||||||
 | 
					        'SessionStorage',
 | 
				
			||||||
 | 
					        'Dialog',
 | 
				
			||||||
 | 
					        'Loading',
 | 
				
			||||||
 | 
					        'Notify',
 | 
				
			||||||
 | 
					        'LoadingBar'
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // animations: 'all', // --- includes all animations
 | 
					    // animations: 'all', // --- includes all animations
 | 
				
			||||||
| 
						 | 
					@ -141,7 +145,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
 | 
					    // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
 | 
				
			||||||
    ssr: {
 | 
					    ssr: {
 | 
				
			||||||
      pwa: false,
 | 
					      pwa: false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
 | 
					    // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
 | 
				
			||||||
| 
						 | 
					@ -160,20 +164,20 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            src: 'flaschengeist-logo.svg',
 | 
					            src: 'flaschengeist-logo.svg',
 | 
				
			||||||
            sizes: 'any',
 | 
					            sizes: 'any',
 | 
				
			||||||
            type: 'image/svg+xml',
 | 
					            type: 'image/svg+xml'
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            src: 'favicon-128x128.png',
 | 
					            src: 'favicon-128x128.png',
 | 
				
			||||||
            sizes: '128x128',
 | 
					            sizes: '128x128',
 | 
				
			||||||
            type: 'image/png',
 | 
					            type: 'image/png'
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            src: 'favicon-256x256.png',
 | 
					            src: 'favicon-256x256.png',
 | 
				
			||||||
            sizes: '256x256',
 | 
					            sizes: '256x256',
 | 
				
			||||||
            type: 'image/png',
 | 
					            type: 'image/png'
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
 | 
					    // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
 | 
				
			||||||
| 
						 | 
					@ -183,7 +187,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
 | 
					    // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
 | 
				
			||||||
    capacitor: {
 | 
					    capacitor: {
 | 
				
			||||||
      hideSplashscreen: true,
 | 
					      hideSplashscreen: true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
 | 
					    // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
 | 
				
			||||||
| 
						 | 
					@ -192,11 +196,13 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      packager: {
 | 
					      packager: {
 | 
				
			||||||
        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
 | 
					        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // OS X / Mac App Store
 | 
					        // OS X / Mac App Store
 | 
				
			||||||
        // appBundleId: '',
 | 
					        // appBundleId: '',
 | 
				
			||||||
        // appCategoryType: '',
 | 
					        // appCategoryType: '',
 | 
				
			||||||
        // osxSign: '',
 | 
					        // osxSign: '',
 | 
				
			||||||
        // protocol: 'myapp://path',
 | 
					        // protocol: 'myapp://path',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Windows only
 | 
					        // Windows only
 | 
				
			||||||
        // win32metadata: { ... }
 | 
					        // win32metadata: { ... }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -204,16 +210,16 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      builder: {
 | 
					      builder: {
 | 
				
			||||||
        // https://www.electron.build/configuration/configuration
 | 
					        // https://www.electron.build/configuration/configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        appId: 'flaschengeist-frontend',
 | 
					        appId: 'flaschengeist-frontend'
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
 | 
					      // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
 | 
				
			||||||
      nodeIntegration: true,
 | 
					      nodeIntegration: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      extendWebpack(/* cfg */) {
 | 
					      extendWebpack (/* cfg */) {
 | 
				
			||||||
        // do something with Electron main process Webpack cfg
 | 
					        // do something with Electron main process Webpack cfg
 | 
				
			||||||
        // chainWebpack also available besides this extendWebpack
 | 
					        // chainWebpack also available besides this extendWebpack
 | 
				
			||||||
      },
 | 
					      }
 | 
				
			||||||
    },
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "appId": "dev.flaschengeist",
 | 
					 | 
				
			||||||
  "appName": "flaschengeist-frontend",
 | 
					 | 
				
			||||||
  "bundledWebRuntime": false,
 | 
					 | 
				
			||||||
  "npmClient": "yarn",
 | 
					 | 
				
			||||||
  "webDir": "www",
 | 
					 | 
				
			||||||
  "ios": {
 | 
					 | 
				
			||||||
    "allowsLinkPreview": false
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "name": "flaschengeist",
 | 
					 | 
				
			||||||
  "version": "2.0.0-alpha.1",
 | 
					 | 
				
			||||||
  "description": "Modular student club administration system",
 | 
					 | 
				
			||||||
  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
					 | 
				
			||||||
  "private": true,
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "@capacitor/android": "^3.3.2",
 | 
					 | 
				
			||||||
    "@capacitor/app": "^1.0.0",
 | 
					 | 
				
			||||||
    "@capacitor/cli": "^3.0.0",
 | 
					 | 
				
			||||||
    "@capacitor/core": "^3.0.0",
 | 
					 | 
				
			||||||
    "@capacitor/ios": "^3.0.0-beta.0",
 | 
					 | 
				
			||||||
    "@capacitor/splash-screen": "^1.0.0",
 | 
					 | 
				
			||||||
    "@capacitor/storage": "^1.2.3"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Generated by package manager
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Generated by Cordova
 | 
				
			||||||
 | 
					/plugins/
 | 
				
			||||||
 | 
					/platforms/
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					<?xml version='1.0' encoding='utf-8'?>
 | 
				
			||||||
 | 
					<widget id="de.wu5.flaschengeist" version="2.0.0-alpha.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
 | 
				
			||||||
 | 
					    <name>flaschengeist-frontend</name>
 | 
				
			||||||
 | 
					    <description>Modular student club administration system</description>
 | 
				
			||||||
 | 
					    <author email="dev@cordova.apache.org" href="http://cordova.io">
 | 
				
			||||||
 | 
					        Apache Cordova Team
 | 
				
			||||||
 | 
					    </author>
 | 
				
			||||||
 | 
					    <content src="index.html" />
 | 
				
			||||||
 | 
					    <access origin="*" />
 | 
				
			||||||
 | 
					    <allow-intent href="http://*/*" />
 | 
				
			||||||
 | 
					    <allow-intent href="https://*/*" />
 | 
				
			||||||
 | 
					    <allow-intent href="tel:*" />
 | 
				
			||||||
 | 
					    <allow-intent href="sms:*" />
 | 
				
			||||||
 | 
					    <allow-intent href="mailto:*" />
 | 
				
			||||||
 | 
					    <allow-intent href="geo:*" />
 | 
				
			||||||
 | 
					    <platform name="android">
 | 
				
			||||||
 | 
					        <allow-intent href="market:*" />
 | 
				
			||||||
 | 
					    </platform>
 | 
				
			||||||
 | 
					    <platform name="ios">
 | 
				
			||||||
 | 
					        <allow-intent href="itms:*" />
 | 
				
			||||||
 | 
					        <allow-intent href="itms-apps:*" />
 | 
				
			||||||
 | 
					    </platform>
 | 
				
			||||||
 | 
					    <allow-navigation href="about:*" />
 | 
				
			||||||
 | 
					</widget>
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,6 @@ import 'quasar/dist/types/feature-flag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module 'quasar/dist/types/feature-flag' {
 | 
					declare module 'quasar/dist/types/feature-flag' {
 | 
				
			||||||
  interface QuasarFeatureFlags {
 | 
					  interface QuasarFeatureFlags {
 | 
				
			||||||
    capacitor: true;
 | 
					    cordova: true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "dev.flaschengeist",
 | 
				
			||||||
 | 
					  "displayName": "flaschengeist-frontend",
 | 
				
			||||||
 | 
					  "version": "1.0.0",
 | 
				
			||||||
 | 
					  "description": "A sample Apache Cordova application that responds to the deviceready event.",
 | 
				
			||||||
 | 
					  "main": "index.js",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "test": "echo \"Error: no test specified\" && exit 1"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "keywords": [
 | 
				
			||||||
 | 
					    "ecosystem:cordova"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "author": "Apache Cordova Team",
 | 
				
			||||||
 | 
					  "license": "Apache-2.0",
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "cordova-android": "^9.1.0",
 | 
				
			||||||
 | 
					    "cordova-ios": "^6.1.1",
 | 
				
			||||||
 | 
					    "cordova-plugin-nativestorage": "^2.3.2",
 | 
				
			||||||
 | 
					    "cordova-plugin-whitelist": "^1.3.4"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "cordova": {
 | 
				
			||||||
 | 
					    "plugins": {
 | 
				
			||||||
 | 
					      "cordova-plugin-whitelist": {},
 | 
				
			||||||
 | 
					      "cordova-plugin-nativestorage": {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "platforms": [
 | 
				
			||||||
 | 
					      "android"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,31 +1,19 @@
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This boot file registers interceptors for axios
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import { useMainStore, api } from '@flaschengeist/api';
 | 
					import { useMainStore, api } from '@flaschengeist/api';
 | 
				
			||||||
 | 
					import { LocalStorage, Notify } from 'quasar';
 | 
				
			||||||
import { AxiosError } from 'axios';
 | 
					import { AxiosError } from 'axios';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
import config from 'src/config';
 | 
					import config from 'src/config';
 | 
				
			||||||
import { clone } from '@flaschengeist/api';
 | 
					import { clone } from '@flaschengeist/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					function minify(o: unknown, cloned = false) {
 | 
				
			||||||
 * Minify data sent to backend server
 | 
					  if (!cloned) o = clone(o);
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Drop unneeded entities which can be identified by ID.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @param obj Object to minify
 | 
					 | 
				
			||||||
 * @param cloned If this entity is already cloned (JSON En+Decoded)
 | 
					 | 
				
			||||||
 * @returns Minified object (some types are converted, like a Date object is now a ISO string)
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function minify(entity: unknown, cloned = false) {
 | 
					 | 
				
			||||||
  if (!cloned) entity = clone(entity);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (typeof entity === 'object') {
 | 
					  if (typeof o === 'object') {
 | 
				
			||||||
    const obj = entity as { [index: string]: unknown };
 | 
					    const obj = o as { [index: string]: unknown };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const prop in obj) {
 | 
					    for (const prop in obj) {
 | 
				
			||||||
      if (obj.hasOwnProperty(prop) && !!obj[prop]) {
 | 
					      if (obj.hasOwnProperty(prop) && !!obj[prop]) {
 | 
				
			||||||
        if (Array.isArray(obj[prop])) {
 | 
					        if (Array.isArray(obj[prop])) {
 | 
				
			||||||
          // eslint-disable-next-line @typescript-eslint/no-unsafe-return
 | 
					 | 
				
			||||||
          obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
 | 
					          obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
 | 
				
			||||||
        } else if (
 | 
					        } else if (
 | 
				
			||||||
          typeof obj[prop] === 'object' &&
 | 
					          typeof obj[prop] === 'object' &&
 | 
				
			||||||
| 
						 | 
					@ -39,12 +27,11 @@ function minify(entity: unknown, cloned = false) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return obj;
 | 
					    return obj;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return entity;
 | 
					  return o;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ router }) => {
 | 
					export default boot(({ router }) => {
 | 
				
			||||||
  // Persisted value is read in plugins.ts boot file!
 | 
					  api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
 | 
				
			||||||
  if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /***
 | 
					  /***
 | 
				
			||||||
   * Intercept requests
 | 
					   * Intercept requests
 | 
				
			||||||
| 
						 | 
					@ -54,9 +41,7 @@ export default boot(({ router }) => {
 | 
				
			||||||
  api.interceptors.request.use((config) => {
 | 
					  api.interceptors.request.use((config) => {
 | 
				
			||||||
    const store = useMainStore();
 | 
					    const store = useMainStore();
 | 
				
			||||||
    if (store.session?.token) {
 | 
					    if (store.session?.token) {
 | 
				
			||||||
      config.headers = Object.assign(config.headers || {}, {
 | 
					      config.headers = { Authorization: 'Bearer ' + store.session.token };
 | 
				
			||||||
        Authorization: `Bearer ${store.session.token}`,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // Minify JSON requests
 | 
					    // Minify JSON requests
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
| 
						 | 
					@ -94,11 +79,12 @@ export default boot(({ router }) => {
 | 
				
			||||||
            query: { redirect: next },
 | 
					            query: { redirect: next },
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } else if (e.response && e.response.status == 401) {
 | 
					        } else if (e.response && e.response.status == 401) {
 | 
				
			||||||
          store.handleLoggedOut();
 | 
					          void store.handleLoggedOut();
 | 
				
			||||||
          if (current.name != 'login') {
 | 
					          if (current.name !== 'login') {
 | 
				
			||||||
            await router.push({
 | 
					            await router.push({
 | 
				
			||||||
              name: 'login',
 | 
					              name: 'login',
 | 
				
			||||||
              query: { redirect: current.fullPath },
 | 
					              params: { logout: 'logout' },
 | 
				
			||||||
 | 
					              query: { redirect: current.path },
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -107,3 +93,19 @@ export default boot(({ router }) => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { api };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setBaseURL = (url: string) => {
 | 
				
			||||||
 | 
					  LocalStorage.set('baseURL', url);
 | 
				
			||||||
 | 
					  api.defaults.baseURL = url;
 | 
				
			||||||
 | 
					  Notify.create({
 | 
				
			||||||
 | 
					    message: 'Serveraddresse gespeichert',
 | 
				
			||||||
 | 
					    position: 'bottom',
 | 
				
			||||||
 | 
					    caption: `${url}`,
 | 
				
			||||||
 | 
					    color: 'positive',
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  setTimeout(() => {
 | 
				
			||||||
 | 
					    window.location.reload();
 | 
				
			||||||
 | 
					  }, 5000);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,87 +0,0 @@
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This boot file initalizes the store from persistent storage and load all plugins
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  PersistentStorage,
 | 
					 | 
				
			||||||
  api,
 | 
					 | 
				
			||||||
  isAxiosError,
 | 
					 | 
				
			||||||
  saveSession,
 | 
					 | 
				
			||||||
  useMainStore,
 | 
					 | 
				
			||||||
} from '@flaschengeist/api';
 | 
					 | 
				
			||||||
import { Notify, Platform } from 'quasar';
 | 
					 | 
				
			||||||
import { loadPlugins } from './plugins';
 | 
					 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					 | 
				
			||||||
import routes from 'src/router/routes';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function loadBaseUrl() {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const url = await PersistentStorage.get<string>('baseURL');
 | 
					 | 
				
			||||||
    if (url !== null) api.defaults.baseURL = url;
 | 
					 | 
				
			||||||
  } catch (e) {
 | 
					 | 
				
			||||||
    console.warn('Could not load BaseURL', e);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BackendError extends Error {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Loading backend information
 | 
					 | 
				
			||||||
 * @returns Backend object or null
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
async function getBackend() {
 | 
					 | 
				
			||||||
  const { data } = await api.get<FG.Backend>('/');
 | 
					 | 
				
			||||||
  if (!data || typeof data !== 'object' || !('plugins' in data))
 | 
					 | 
				
			||||||
    throw new BackendError('Invalid backend response received');
 | 
					 | 
				
			||||||
  return data;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Boot file for loading baseURL + Session from PersistentStorage + loading and initializing all plugins
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export default boot(async ({ app, router }) => {
 | 
					 | 
				
			||||||
  const store = useMainStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // FIRST(!) get the base URL
 | 
					 | 
				
			||||||
  await loadBaseUrl();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Init the store, load current session and user, if available
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    await store.init();
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    // Any changes on the session is written back to the persistent store
 | 
					 | 
				
			||||||
    store.$subscribe((mutation, state) => {
 | 
					 | 
				
			||||||
      saveSession(state.session);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Load all plugins
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    // Fetch backend data
 | 
					 | 
				
			||||||
    const backend = await getBackend();
 | 
					 | 
				
			||||||
    // Load enabled plugins
 | 
					 | 
				
			||||||
    const flaschengeist = await loadPlugins(backend, routes);
 | 
					 | 
				
			||||||
    // Add loaded routes to router
 | 
					 | 
				
			||||||
    flaschengeist.routes.forEach((route) => router.addRoute(route));
 | 
					 | 
				
			||||||
    // save plugins in VM-variable
 | 
					 | 
				
			||||||
    app.provide('flaschengeist', flaschengeist);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    // Handle errors from loading the backend information
 | 
					 | 
				
			||||||
    if (error instanceof BackendError || isAxiosError(error)) {
 | 
					 | 
				
			||||||
      router.isReady().finally(() => {
 | 
					 | 
				
			||||||
        if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
 | 
					 | 
				
			||||||
        else void router.push({ name: 'offline', params: { refresh: 1 } });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    } else if (typeof error === 'string') {
 | 
					 | 
				
			||||||
      // Handle plugin not found errors
 | 
					 | 
				
			||||||
      void router.push({ name: 'error' });
 | 
					 | 
				
			||||||
      Notify.create({
 | 
					 | 
				
			||||||
        type: 'negative',
 | 
					 | 
				
			||||||
        message: `Fehler beim Laden: Bitte wende dich an den Admin (${error})!`,
 | 
					 | 
				
			||||||
        timeout: 10000,
 | 
					 | 
				
			||||||
        progress: true,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      console.error('Unknown error in init.ts:', error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import { Loading } from 'quasar';
 | 
				
			||||||
 | 
					//import DarkCircularProgress from 'components/loading/DarkCircularProgress.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// "async" is optional;
 | 
				
			||||||
 | 
					// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
 | 
				
			||||||
 | 
					export default boot(() => {
 | 
				
			||||||
 | 
					  Loading.setDefaults({
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    // spinner: DarkCircularProgress,
 | 
				
			||||||
 | 
					    // TODO : Das funktioniert wohl erstmal nicht mehr... gibt ne exception
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,33 +1,42 @@
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * This boot file registers login / authentification related axios interceptors
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import { useMainStore, hasPermissions } from '@flaschengeist/api';
 | 
					import { useMainStore, hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import { RouteRecord } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ router }) => {
 | 
					export default boot(({ router }) => {
 | 
				
			||||||
  /**
 | 
					  router.beforeResolve((to, from, next) => {
 | 
				
			||||||
   * Login guard
 | 
					 | 
				
			||||||
   * Check if user tries to access the secured area and validates token
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  router.beforeEach((to, from) => {
 | 
					 | 
				
			||||||
    const store = useMainStore();
 | 
					    const store = useMainStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Skip loops
 | 
					    if (to.path == from.path) return next();
 | 
				
			||||||
    if (to.name == 'login' && from.name == 'login') return false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Secured area '/in/...' requires to be authenticated
 | 
					    if (to.path.startsWith('/main')) {
 | 
				
			||||||
    if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
 | 
					      // Secured area (LOGIN REQUIRED)
 | 
				
			||||||
      store.handleLoggedOut();
 | 
					      // Check login is ok
 | 
				
			||||||
      return { name: 'login' };
 | 
					      if (!store.session || store.session.expires <= new Date()) {
 | 
				
			||||||
 | 
					        void store.handleLoggedOut();
 | 
				
			||||||
 | 
					        return next({ name: 'login', query: { redirect: to.fullPath } });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Check if special permissions are required
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        to.matched.every((record: RouteRecord) => {
 | 
				
			||||||
 | 
					          if (!('meta' in record) || !('permissions' in record.meta)) return true;
 | 
				
			||||||
 | 
					          if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
 | 
				
			||||||
 | 
					            return hasPermissions((<{ permissions: FG.Permission[] }>record.meta).permissions);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return next({ name: 'login', query: { redirect: to.fullPath } });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (to.name == 'login' && store.user && !to.params['logout']) {
 | 
				
			||||||
 | 
					        // Called login while already logged in
 | 
				
			||||||
 | 
					        return next({ name: 'dashboard' });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // We are on the non secured area
 | 
				
			||||||
 | 
					        return next();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Permission guard
 | 
					 | 
				
			||||||
   * Check permissions for route, cancel navigation on errors
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  router.beforeResolve((to) => {
 | 
					 | 
				
			||||||
    if (!!to.meta.permissions && !hasPermissions(<FG.Permission[]>to.meta.permissions))
 | 
					 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,10 @@
 | 
				
			||||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
					import { Notify } from 'quasar';
 | 
				
			||||||
 | 
					import { api } from 'src/boot/axios';
 | 
				
			||||||
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import routes from 'src/router/routes';
 | 
				
			||||||
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
import { RouteRecordRaw } from 'vue-router';
 | 
					import { RouteRecordRaw } from 'vue-router';
 | 
				
			||||||
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/****************************************************
 | 
					/****************************************************
 | 
				
			||||||
 ******** Internal area for some magic **************
 | 
					 ******** Internal area for some magic **************
 | 
				
			||||||
| 
						 | 
					@ -21,6 +26,21 @@ const PLUGINS = <Array<Promise<ImportPlgn>>>[
 | 
				
			||||||
  /*INSERT_PLUGIN_LIST*/
 | 
					  /*INSERT_PLUGIN_LIST*/
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BackendPlugin {
 | 
				
			||||||
 | 
					  permissions: string[];
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BackendPlugins {
 | 
				
			||||||
 | 
					  [key: string]: BackendPlugin;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Backend {
 | 
				
			||||||
 | 
					  plugins: BackendPlugins;
 | 
				
			||||||
 | 
					  version: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export { Backend };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Handle Notifications
 | 
					// Handle Notifications
 | 
				
			||||||
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
 | 
					export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -192,7 +212,7 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
 | 
				
			||||||
function loadPlugin(
 | 
					function loadPlugin(
 | 
				
			||||||
  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
					  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
				
			||||||
  plugin: FG_Plugin.Plugin,
 | 
					  plugin: FG_Plugin.Plugin,
 | 
				
			||||||
  backend: FG.Backend
 | 
					  backend: Backend
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  // Check if already loaded
 | 
					  // Check if already loaded
 | 
				
			||||||
  if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
 | 
					  if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
 | 
				
			||||||
| 
						 | 
					@ -244,9 +264,33 @@ function loadPlugin(
 | 
				
			||||||
  return true;
 | 
					  return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
 | 
					/**
 | 
				
			||||||
 | 
					 * Loading backend information
 | 
				
			||||||
 | 
					 * @returns Backend object or null
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function getBackend() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data }: AxiosResponse<Backend> = await api.get('/');
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.warn(e);
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Boot file, load all required plugins, check for dependencies
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default boot(async ({ router, app }) => {
 | 
				
			||||||
 | 
					  const backend = await getBackend();
 | 
				
			||||||
 | 
					  if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
 | 
				
			||||||
 | 
					    console.log('Backend error');
 | 
				
			||||||
 | 
					    router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const loadedPlugins: FG_Plugin.Flaschengeist = {
 | 
					  const loadedPlugins: FG_Plugin.Flaschengeist = {
 | 
				
			||||||
    routes: baseRoutes,
 | 
					    routes,
 | 
				
			||||||
    plugins: [],
 | 
					    plugins: [],
 | 
				
			||||||
    menuLinks: [],
 | 
					    menuLinks: [],
 | 
				
			||||||
    shortcuts: [],
 | 
					    shortcuts: [],
 | 
				
			||||||
| 
						 | 
					@ -254,35 +298,50 @@ export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRa
 | 
				
			||||||
    widgets: [],
 | 
					    widgets: [],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Wait for all plugins to be loaded
 | 
					  try {
 | 
				
			||||||
  const results = await Promise.allSettled(PLUGINS);
 | 
					    // Wait for all plugins to be loaded
 | 
				
			||||||
 | 
					    const results = await Promise.allSettled(PLUGINS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check if loaded successfully
 | 
					    // Check if loaded successfully
 | 
				
			||||||
  results.forEach((result) => {
 | 
					    results.forEach((result) => {
 | 
				
			||||||
    if (result.status === 'rejected') {
 | 
					      if (result.status === 'rejected') {
 | 
				
			||||||
      throw <string>result.reason;
 | 
					        throw <string>result.reason;
 | 
				
			||||||
    } else {
 | 
					      } else {
 | 
				
			||||||
      if (
 | 
					        if (
 | 
				
			||||||
        !(
 | 
					          !(
 | 
				
			||||||
          validatePlugin(result.value.default) &&
 | 
					            validatePlugin(result.value.default) &&
 | 
				
			||||||
          loadPlugin(loadedPlugins, result.value.default, backend)
 | 
					            loadPlugin(loadedPlugins, result.value.default, backend)
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      )
 | 
					          throw result.value.default.id;
 | 
				
			||||||
        throw result.value.default.id;
 | 
					      }
 | 
				
			||||||
    }
 | 
					    });
 | 
				
			||||||
  });
 | 
					  } catch (reason) {
 | 
				
			||||||
 | 
					    const id = <string>reason;
 | 
				
			||||||
 | 
					    void router.push({ name: 'error' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Notify.create({
 | 
				
			||||||
 | 
					      type: 'negative',
 | 
				
			||||||
 | 
					      message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${id}!`,
 | 
				
			||||||
 | 
					      timeout: 10000,
 | 
				
			||||||
 | 
					      progress: true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Sort widgets by priority
 | 
					  // Sort widgets by priority
 | 
				
			||||||
  /** @todo Remove priority with first beta */
 | 
					  /** @todo Remove priority with first beta */
 | 
				
			||||||
  loadedPlugins.widgets.sort(
 | 
					  loadedPlugins.widgets.sort(
 | 
				
			||||||
    (a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
 | 
					    (a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** @todo Can be cleaned up with first beta */
 | 
					  /** @todo Can be cleaned up with first beta */
 | 
				
			||||||
  loadedPlugins.menuLinks.sort((a, b) => {
 | 
					  loadedPlugins.menuLinks.sort((a, b) => {
 | 
				
			||||||
    const diff = a.order && b.order ? b.order - a.order : 0;
 | 
					    const diff = a.order && b.order ? b.order - a.order : 0;
 | 
				
			||||||
    return diff ? diff : a.title.toString().localeCompare(b.title.toString());
 | 
					    return diff ? diff : a.title.toString().localeCompare(b.title.toString());
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return loadedPlugins;
 | 
					  // Add loaded routes to router
 | 
				
			||||||
}
 | 
					  loadedPlugins.routes.forEach((route) => router.addRoute(route));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // save plugins in VM-variable
 | 
				
			||||||
 | 
					  app.provide('flaschengeist', loadedPlugins);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,50 @@
 | 
				
			||||||
/**
 | 
					import { LocalStorage } from 'quasar';
 | 
				
			||||||
 * This boot file installs the global pinia instance
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
import { pinia } from '@flaschengeist/api';
 | 
					 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import { useMainStore, pinia } from '@flaschengeist/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isCordova() {
 | 
				
			||||||
 | 
					  return window.hasOwnProperty('cordova');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function error() {
 | 
				
			||||||
 | 
					  console.debug('NativeStorage Error');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ app }) => {
 | 
					export default boot(({ app }) => {
 | 
				
			||||||
  app.use(pinia);
 | 
					  app.use(pinia);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  document.addEventListener('deviceready', () => {
 | 
				
			||||||
 | 
					    const store = useMainStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Load main state from storage
 | 
				
			||||||
 | 
					    if (isCordova()) {
 | 
				
			||||||
 | 
					      // is cordova, so use Native Storage
 | 
				
			||||||
 | 
					      window.NativeStorage.keys((keys) => {
 | 
				
			||||||
 | 
					        // Check if key is set
 | 
				
			||||||
 | 
					        if (keys.includes('main_store'))
 | 
				
			||||||
 | 
					          window.NativeStorage.getItem<typeof store.$state>(
 | 
				
			||||||
 | 
					            'main_store',
 | 
				
			||||||
 | 
					            (v) => {
 | 
				
			||||||
 | 
					              // Check is object, then patch it
 | 
				
			||||||
 | 
					              if (typeof v === 'object') store.$patch(v);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					      }, error);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // is not cordova, so use LocalStorage
 | 
				
			||||||
 | 
					      store.$patch(LocalStorage.getItem<typeof store.$state>('main_store') || {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Subscript to save changes of state to storage
 | 
				
			||||||
 | 
					    store.$subscribe(
 | 
				
			||||||
 | 
					      (mutation, state) => {
 | 
				
			||||||
 | 
					        if (isCordova()) window.NativeStorage.setItem('main_store', state, () => undefined, error);
 | 
				
			||||||
 | 
					        else LocalStorage.set('main_store', state);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { deep: true }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void store.init();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,67 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @name Native Storage
 | 
				
			||||||
 | 
					 * @premier nativestorage
 | 
				
			||||||
 | 
					 * @description Native storage of variables in Android and iOS
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @usage
 | 
				
			||||||
 | 
					 * ```typescript
 | 
				
			||||||
 | 
					 * import { NativeStorage } from '@ionic-native/native-storage/ngx';
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * constructor(private nativeStorage: NativeStorage) { }
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * ...
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * this.nativeStorage.setItem('myitem', {property: 'value', anotherProperty: 'anotherValue'})
 | 
				
			||||||
 | 
					 *   .then(
 | 
				
			||||||
 | 
					 *     () => console.log('Stored item!'),
 | 
				
			||||||
 | 
					 *     error => console.error('Error storing item', error)
 | 
				
			||||||
 | 
					 *   );
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * this.nativeStorage.getItem('myitem')
 | 
				
			||||||
 | 
					 *   .then(
 | 
				
			||||||
 | 
					 *     data => console.log(data),
 | 
				
			||||||
 | 
					 *     error => console.error(error)
 | 
				
			||||||
 | 
					 *   );
 | 
				
			||||||
 | 
					 * ```
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare class NativeStorageOriginal {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Initialises shared storage with the suite name when using app groups in iOS
 | 
				
			||||||
 | 
					   * @param reference {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  initWithSuiteName(reference: string, success: () => void, error: () => void): void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Stores a value
 | 
				
			||||||
 | 
					   * @param reference {string}
 | 
				
			||||||
 | 
					   * @param value
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  setItem(reference: string, value: unknown, success: () => void, error: () => void): void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Gets a stored item
 | 
				
			||||||
 | 
					   * @param reference {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getItem<T = unknown>(reference: string, success: (obj: T) => void, error: () => void): void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Retrieving all keys
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  keys(success: (keys: string[]) => void, error: () => void): void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Removes a single stored item
 | 
				
			||||||
 | 
					   * @param reference {string}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  remove(reference: string, success: () => void, error: () => void): void;
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Removes all stored values.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  clear(success: () => void, error: () => void): void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare const NativeStorage: NativeStorageOriginal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface Window {
 | 
				
			||||||
 | 
					    NativeStorage: NativeStorageOriginal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -40,14 +40,7 @@
 | 
				
			||||||
            <shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" />
 | 
					            <shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" />
 | 
				
			||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
        </drag>
 | 
					        </drag>
 | 
				
			||||||
        <q-btn
 | 
					        <q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" />
 | 
				
			||||||
          v-if="!platform.is.capacitor"
 | 
					 | 
				
			||||||
          flat
 | 
					 | 
				
			||||||
          round
 | 
					 | 
				
			||||||
          dense
 | 
					 | 
				
			||||||
          icon="mdi-exit-to-app"
 | 
					 | 
				
			||||||
          @click="logout()"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-toolbar>
 | 
					      </q-toolbar>
 | 
				
			||||||
    </q-header>
 | 
					    </q-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,18 +64,6 @@
 | 
				
			||||||
        :key="'essential' + index"
 | 
					        :key="'essential' + index"
 | 
				
			||||||
        :entry="entry"
 | 
					        :entry="entry"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div v-if="platform.is.capacitor">
 | 
					 | 
				
			||||||
        <q-separator />
 | 
					 | 
				
			||||||
        <q-item clickable tag="a" target="self" @click="logout">
 | 
					 | 
				
			||||||
          <q-item-section avatar>
 | 
					 | 
				
			||||||
            <q-icon name="mdi-exit-to-app" />
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <q-item-section>
 | 
					 | 
				
			||||||
            <q-item-label>Logout</q-item-label>
 | 
					 | 
				
			||||||
          </q-item-section>
 | 
					 | 
				
			||||||
        </q-item>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-drawer>
 | 
					    </q-drawer>
 | 
				
			||||||
    <q-page-container>
 | 
					    <q-page-container>
 | 
				
			||||||
      <router-view />
 | 
					      <router-view />
 | 
				
			||||||
| 
						 | 
					@ -220,7 +201,6 @@ export default defineComponent({
 | 
				
			||||||
      shortCuts,
 | 
					      shortCuts,
 | 
				
			||||||
      addShortcut,
 | 
					      addShortcut,
 | 
				
			||||||
      deleteShortcut,
 | 
					      deleteShortcut,
 | 
				
			||||||
      platform: Platform,
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,64 +2,63 @@
 | 
				
			||||||
  <q-page padding class="fit row justify-center items-center content-center">
 | 
					  <q-page padding class="fit row justify-center items-center content-center">
 | 
				
			||||||
    <q-card class="col-xs-11 col-sm-8 col-md-6 col-lg-4 justify-center items-center content-center">
 | 
					    <q-card class="col-xs-11 col-sm-8 col-md-6 col-lg-4 justify-center items-center content-center">
 | 
				
			||||||
      <q-toolbar class="bg-primary text-white">
 | 
					      <q-toolbar class="bg-primary text-white">
 | 
				
			||||||
        <q-toolbar-title>{{ backendSetup ? 'Servereinrichtung' : 'Login' }}</q-toolbar-title>
 | 
					        <q-toolbar-title> Login </q-toolbar-title>
 | 
				
			||||||
      </q-toolbar>
 | 
					      </q-toolbar>
 | 
				
			||||||
      <div v-if="!backendSetup">
 | 
					
 | 
				
			||||||
        <q-card-section>
 | 
					      <q-card-section>
 | 
				
			||||||
          <q-form @submit="doLogin">
 | 
					        <q-form @submit="doLogin">
 | 
				
			||||||
            <q-input
 | 
					          <q-input
 | 
				
			||||||
              v-model="userid"
 | 
					            v-model="userid"
 | 
				
			||||||
              class="q-pa-md"
 | 
					            class="q-pa-md"
 | 
				
			||||||
              filled
 | 
					            filled
 | 
				
			||||||
              label="Benutzername oder E-Mail"
 | 
					            label="Benutzername oder E-Mail"
 | 
				
			||||||
              autocomplete="username"
 | 
					            autocomplete="username"
 | 
				
			||||||
              :rules="[notEmpty]"
 | 
					            :rules="[notEmpty]"
 | 
				
			||||||
              tabindex="1"
 | 
					            tabindex="1"
 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <password-input
 | 
					 | 
				
			||||||
              v-model="password"
 | 
					 | 
				
			||||||
              class="q-px-md q-pt-md q-pb-none"
 | 
					 | 
				
			||||||
              filled
 | 
					 | 
				
			||||||
              label="Passwort"
 | 
					 | 
				
			||||||
              autocomplete="cureent-password"
 | 
					 | 
				
			||||||
              :rules="[notEmpty]"
 | 
					 | 
				
			||||||
              tabindex="2"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <div class="full-width row justify-end q-px-md">
 | 
					 | 
				
			||||||
              <q-btn
 | 
					 | 
				
			||||||
                label="Passwort vergessen?"
 | 
					 | 
				
			||||||
                type="a"
 | 
					 | 
				
			||||||
                color="secondary"
 | 
					 | 
				
			||||||
                tabindex="4"
 | 
					 | 
				
			||||||
                flat
 | 
					 | 
				
			||||||
                dense
 | 
					 | 
				
			||||||
                size="sm"
 | 
					 | 
				
			||||||
                @click="doReset"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="full-width row justify-end q-px-md q-pt-md">
 | 
					 | 
				
			||||||
              <q-btn label="Login" type="submit" color="primary" tabindex="3" />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </q-form>
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
        <div class="row justify-end">
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            v-if="$q.platform.is.capacitor || $q.platform.is.electron"
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            round
 | 
					 | 
				
			||||||
            :icon="showBackendSetup ? 'mdi-menu-up' : 'mdi-menu-down'"
 | 
					 | 
				
			||||||
            @click="showBackendSetup = !showBackendSetup"
 | 
					 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					          <password-input
 | 
				
			||||||
 | 
					            v-model="password"
 | 
				
			||||||
 | 
					            class="q-px-md q-pt-md q-pb-none"
 | 
				
			||||||
 | 
					            filled
 | 
				
			||||||
 | 
					            label="Passwort"
 | 
				
			||||||
 | 
					            autocomplete="cureent-password"
 | 
				
			||||||
 | 
					            :rules="[notEmpty]"
 | 
				
			||||||
 | 
					            tabindex="2"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <div class="full-width row justify-end q-px-md">
 | 
				
			||||||
 | 
					            <q-btn
 | 
				
			||||||
 | 
					              label="Passwort vergessen?"
 | 
				
			||||||
 | 
					              type="a"
 | 
				
			||||||
 | 
					              color="secondary"
 | 
				
			||||||
 | 
					              tabindex="4"
 | 
				
			||||||
 | 
					              flat
 | 
				
			||||||
 | 
					              dense
 | 
				
			||||||
 | 
					              size="sm"
 | 
				
			||||||
 | 
					              @click="doReset"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="full-width row justify-end q-px-md q-pt-md">
 | 
				
			||||||
 | 
					            <q-btn label="Login" type="submit" color="primary" tabindex="3" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </q-form>
 | 
				
			||||||
 | 
					      </q-card-section>
 | 
				
			||||||
 | 
					      <div class="row justify-end">
 | 
				
			||||||
 | 
					        <q-btn
 | 
				
			||||||
 | 
					          v-if="quasar.platform.is.cordova || quasar.platform.is.electron"
 | 
				
			||||||
 | 
					          flat
 | 
				
			||||||
 | 
					          round
 | 
				
			||||||
 | 
					          icon="mdi-menu-down"
 | 
				
			||||||
 | 
					          @click="openServerSettings"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <q-slide-transition v-if="$q.platform.is.capacitor">
 | 
					      <q-slide-transition v-if="quasar.platform.is.cordova || quasar.platform.is.electron">
 | 
				
			||||||
        <div v-show="showBackendSetup || backendSetup">
 | 
					        <div v-show="visible">
 | 
				
			||||||
          <q-separator />
 | 
					          <q-separator />
 | 
				
			||||||
          <q-card-section>
 | 
					          <q-card-section>
 | 
				
			||||||
            <q-form ref="ServerSettingsForm" class="q-gutter-md" @submit="changeBackend">
 | 
					            <q-form ref="ServerSettingsForm" class="q-gutter-md" @submit="changeUrl">
 | 
				
			||||||
              <div class="text-h6">Backend einrichten</div>
 | 
					              <div class="text-h6">Servereinstellung</div>
 | 
				
			||||||
              <q-input v-model="server" filled label="Server" dense />
 | 
					              <q-input v-model="server" filled label="Server" dense />
 | 
				
			||||||
              <q-btn dense color="primary" label="Speichern" type="submit" />
 | 
					              <q-btn size="xs" dense color="primary" label="Speichern" type="submit" />
 | 
				
			||||||
            </q-form>
 | 
					            </q-form>
 | 
				
			||||||
          </q-card-section>
 | 
					          </q-card-section>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					@ -70,45 +69,36 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import { Loading, Notify, useQuasar } from 'quasar';
 | 
					import { Loading, Notify } from 'quasar';
 | 
				
			||||||
import { api, notEmpty, PersistentStorage, useMainStore } from '@flaschengeist/api';
 | 
					import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
 | 
				
			||||||
import { PasswordInput } from '@flaschengeist/api/components';
 | 
					import { PasswordInput } from '@flaschengeist/api/components';
 | 
				
			||||||
import { defineComponent, onMounted, ref } from 'vue';
 | 
					import { defineComponent, ref } from 'vue';
 | 
				
			||||||
import { useSessionStore } from 'app/api';
 | 
					import { setBaseURL, api } from 'boot/axios';
 | 
				
			||||||
 | 
					import { useQuasar } from 'quasar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'PageLogin',
 | 
					  name: 'PageLogin',
 | 
				
			||||||
  components: { PasswordInput },
 | 
					  components: { PasswordInput },
 | 
				
			||||||
  props: {
 | 
					  setup() {
 | 
				
			||||||
    backendSetup: {
 | 
					 | 
				
			||||||
      type: Boolean,
 | 
					 | 
				
			||||||
      default: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props) {
 | 
					 | 
				
			||||||
    const mainRoute = { name: 'dashboard' };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					    const mainStore = useMainStore();
 | 
				
			||||||
    const sessionStore = useSessionStore();
 | 
					    const userStore = useUserStore();
 | 
				
			||||||
 | 
					    const mainRoute = { name: 'dashboard' };
 | 
				
			||||||
    const router = useRouter();
 | 
					    const router = useRouter();
 | 
				
			||||||
    const quasar = useQuasar();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMounted(() => {
 | 
					 | 
				
			||||||
      if (mainStore.session) void router.replace(mainRoute);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Stuff for the real login page */
 | 
					    /* Stuff for the real login page */
 | 
				
			||||||
    const userid = ref('');
 | 
					    const userid = ref('');
 | 
				
			||||||
    const password = ref('');
 | 
					    const password = ref('');
 | 
				
			||||||
    const server = ref(api.defaults.baseURL);
 | 
					    const server = ref<string | undefined>(api.defaults.baseURL);
 | 
				
			||||||
    const showBackendSetup = ref(!!props.backendSetup);
 | 
					    const visible = ref(false);
 | 
				
			||||||
 | 
					    const quasar = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function changeBackend() {
 | 
					    function openServerSettings() {
 | 
				
			||||||
 | 
					      visible.value = !visible.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function changeUrl() {
 | 
				
			||||||
      if (server.value) {
 | 
					      if (server.value) {
 | 
				
			||||||
        void PersistentStorage.set('baseURL', server.value).then(() => {
 | 
					        setBaseURL(server.value);
 | 
				
			||||||
          showBackendSetup.value = false;
 | 
					 | 
				
			||||||
          void router.go(0);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,18 +107,10 @@ export default defineComponent({
 | 
				
			||||||
      const status = await mainStore.login(userid.value, password.value);
 | 
					      const status = await mainStore.login(userid.value, password.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (status === true) {
 | 
					      if (status === true) {
 | 
				
			||||||
        // On capacitor we set the lifetime to at least two weeks to not annoy users.
 | 
					        mainStore.user = (await userStore.getUser(userid.value, true)) || undefined;
 | 
				
			||||||
        if (quasar.platform.is.capacitor)
 | 
					        const x = router.currentRoute.value.query['redirect'];
 | 
				
			||||||
          await sessionStore.updateSession(14 * 24 * 60 * 60, mainStore.currentSession.token);
 | 
					        void router.push(typeof x === 'string' ? { path: x } : mainRoute);
 | 
				
			||||||
        // Redirect user to previous page, if any.
 | 
					 | 
				
			||||||
        const redirect =
 | 
					 | 
				
			||||||
          router.currentRoute.value.redirectedFrom || 'redirect' in router.currentRoute.value.query
 | 
					 | 
				
			||||||
            ? { path: router.currentRoute.value.query.redirect as string }
 | 
					 | 
				
			||||||
            : mainRoute;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        void router.push(redirect);
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        // Login failed, notify and reset form
 | 
					 | 
				
			||||||
        password.value = '';
 | 
					        password.value = '';
 | 
				
			||||||
        if (status === 401) {
 | 
					        if (status === 401) {
 | 
				
			||||||
          Notify.create({
 | 
					          Notify.create({
 | 
				
			||||||
| 
						 | 
					@ -173,14 +155,16 @@ export default defineComponent({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      changeBackend,
 | 
					      changeUrl,
 | 
				
			||||||
      doLogin,
 | 
					      doLogin,
 | 
				
			||||||
      doReset,
 | 
					      doReset,
 | 
				
			||||||
      notEmpty,
 | 
					      notEmpty,
 | 
				
			||||||
 | 
					      openServerSettings,
 | 
				
			||||||
      password,
 | 
					      password,
 | 
				
			||||||
      userid,
 | 
					 | 
				
			||||||
      server,
 | 
					      server,
 | 
				
			||||||
      showBackendSetup,
 | 
					      userid,
 | 
				
			||||||
 | 
					      visible,
 | 
				
			||||||
 | 
					      quasar,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,18 +10,12 @@ const routes: RouteRecordRaw[] = [
 | 
				
			||||||
        name: 'login',
 | 
					        name: 'login',
 | 
				
			||||||
        path: 'login',
 | 
					        path: 'login',
 | 
				
			||||||
        component: () => import('pages/Login.vue'),
 | 
					        component: () => import('pages/Login.vue'),
 | 
				
			||||||
        props: true,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name: 'password_reset',
 | 
					        name: 'password_reset',
 | 
				
			||||||
        path: 'reset',
 | 
					        path: 'reset',
 | 
				
			||||||
        component: () => import('pages/Reset.vue'),
 | 
					        component: () => import('pages/Reset.vue'),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        path: '/setup-backend',
 | 
					 | 
				
			||||||
        name: 'setup_backend',
 | 
					 | 
				
			||||||
        redirect: { name: 'login', params: { backendSetup: 1 } },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name: 'about_out',
 | 
					        name: 'about_out',
 | 
				
			||||||
        path: 'about',
 | 
					        path: 'about',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1,3 @@
 | 
				
			||||||
module.exports = ['@flaschengeist/users'];
 | 
					module.exports = [
 | 
				
			||||||
 | 
					    '@flaschengeist/users',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
		Loading…
	
		Reference in New Issue