Compare commits
	
		
			116 Commits
		
	
	
		
			feature/ba
			...
			main
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						b7735e2924 | |
| 
							
							
								
									
								
								 | 
						d35cc8e8d1 | |
| 
							
							
								
									
								
								 | 
						d34898e1e9 | |
| 
							
							
								
									
								
								 | 
						93669d66dc | |
| 
							
							
								
									
								
								 | 
						ec5458bf7e | |
| 
							
							
								
									
								
								 | 
						efc7c49a0b | |
| 
							
							
								
									
								
								 | 
						b1e4879881 | |
| 
							
							
								
									
								
								 | 
						ee7e03ce28 | |
| 
							
							
								
									
								
								 | 
						2928c241ad | |
| 
							
							
								
									
								
								 | 
						fe9ec96ce1 | |
| 
							
							
								
									
								
								 | 
						417689b725 | |
| 
							
							
								
									
								
								 | 
						847e923265 | |
| 
							
							
								
									
								
								 | 
						4cb0362bb7 | |
| 
							
							
								
									
								
								 | 
						a46c41cb5b | |
| 
							
							
								
									
								
								 | 
						3d55f2d2ae | |
| 
							
							
								
									
								
								 | 
						3689da810c | |
| 
							
							
								
									
								
								 | 
						e6d9054256 | |
| 
							
							
								
									
								
								 | 
						ab45bf3667 | |
| 
							
							
								
									
								
								 | 
						857d07040b | |
| 
							
							
								
								 | 
						e07df08822 | |
| 
							
							
								
								 | 
						ec28857af5 | |
| 
							
							
								
								 | 
						1c452e23fe | |
| 
							
							
								
								 | 
						195593ddc5 | |
| 
							
							
								
								 | 
						2425e6cf2f | |
| 
							
							
								
								 | 
						a9edc12494 | |
| 
							
							
								
								 | 
						1525de1469 | |
| 
							
							
								
								 | 
						6a75d1bf51 | |
| 
							
							
								
								 | 
						f9f66e7172 | |
| 
							
							
								
								 | 
						e9c0086859 | |
| 
							
							
								
								 | 
						b2c70a6657 | |
| 
							
							
								
								 | 
						f27212f60e | |
| 
							
							
								
								 | 
						9eb5412c14 | |
| 
							
							
								
								 | 
						8e552ba508 | |
| 
							
							
								
								 | 
						49c3ec74ba | |
| 
							
							
								
								 | 
						83f32ea82a | |
| 
							
							
								
								 | 
						656d7a9e3c | |
| 
							
							
								
								 | 
						8fca175d39 | |
| 
							
							
								
								 | 
						6c219c5226 | |
| 
							
							
								
								 | 
						cb43b8a39b | |
| 
							
							
								
									
								
								 | 
						acf1816b55 | |
| 
							
							
								
								 | 
						02f60335d4 | |
| 
							
							
								
								 | 
						d9267bcc0a | |
| 
							
							
								
								 | 
						6732921ff7 | |
| 
							
							
								
								 | 
						7a705d5f9a | |
| 
							
							
								
								 | 
						368ca23c56 | |
| 
							
							
								
									
								
								 | 
						d82e025700 | |
| 
							
							
								
									
								
								 | 
						07e1966471 | |
| 
							
							
								
								 | 
						29c085bd2c | |
| 
							
							
								
								 | 
						1b152b52f5 | |
| 
							
							
								
								 | 
						6769e18ffa | |
| 
							
							
								
								 | 
						88dd96c937 | |
| 
							
							
								
								 | 
						4887bc261b | |
| 
							
							
								
								 | 
						d516839ad4 | |
| 
							
							
								
								 | 
						c9df257bbf | |
| 
							
							
								
									
								
								 | 
						053fdae384 | |
| 
							
							
								
								 | 
						6fd3f045f8 | |
| 
							
							
								
								 | 
						2d167ebbae | |
| 
							
							
								
								 | 
						a8578e2803 | |
| 
							
							
								
								 | 
						e4889ddac2 | |
| 
							
							
								
								 | 
						664def40fc | |
| 
							
							
								
								 | 
						ade6d06eb6 | |
| 
							
							
								
									
								
								 | 
						53f053a294 | |
| 
							
							
								
									
								
								 | 
						cc29893e04 | |
| 
							
							
								
								 | 
						42800a9d99 | |
| 
							
							
								
								 | 
						f4650ffdeb | |
| 
							
							
								
								 | 
						1158525abb | |
| 
							
							
								
								 | 
						04e3c57397 | |
| 
							
							
								
									
								
								 | 
						2fc411d51d | |
| 
							
							
								
									
								
								 | 
						6061b37887 | |
| 
							
							
								
									
								
								 | 
						efde9a2ee7 | |
| 
							
							
								
									
								
								 | 
						e96d15bc66 | |
| 
							
							
								
									
								
								 | 
						dad88ec766 | |
| 
							
							
								
								 | 
						3e091fd02b | |
| 
							
							
								
								 | 
						fca79c36ef | |
| 
							
							
								
								 | 
						34fcdbdb7f | |
| 
							
							
								
								 | 
						d84796b09d | |
| 
							
							
								
								 | 
						62af4f5026 | |
| 
							
							
								
									
								
								 | 
						36d6fdfb94 | |
| 
							
							
								
								 | 
						a30da50b1d | |
| 
							
							
								
								 | 
						53951daa25 | |
| 
							
							
								
								 | 
						59920e23a5 | |
| 
							
							
								
								 | 
						5952c9b7f2 | |
| 
							
							
								
								 | 
						73f50e9f4f | |
| 
							
							
								
								 | 
						dfb924bb3f | |
| 
							
							
								
								 | 
						bc9dba1c7b | |
| 
							
							
								
								 | 
						1132bfd129 | |
| 
							
							
								
								 | 
						fe1fae10f5 | |
| 
							
							
								
								 | 
						1c36399ac0 | |
| 
							
							
								
									
								
								 | 
						a38954cf70 | |
| 
							
							
								
								 | 
						35d8433e23 | |
| 
							
							
								
								 | 
						731cc20a06 | |
| 
							
							
								
								 | 
						c38032085f | |
| 
							
							
								
								 | 
						22f47bd34e | |
| 
							
							
								
									
								
								 | 
						16fd9201ae | |
| 
							
							
								
								 | 
						873fee3301 | |
| 
							
							
								
								 | 
						1802081ad2 | |
| 
							
							
								
								 | 
						631e78acb3 | |
| 
							
							
								
								 | 
						d422380adc | |
| 
							
							
								
								 | 
						0c279289b2 | |
| 
							
							
								
								 | 
						979eab05af | |
| 
							
							
								
								 | 
						fd918f5bb7 | |
| 
							
							
								
								 | 
						068dbdcc7b | |
| 
							
							
								
								 | 
						8c9db67b95 | |
| 
							
							
								
								 | 
						86bad83e53 | |
| 
							
							
								
								 | 
						625ac55b0a | |
| 
							
							
								
								 | 
						9940589d1a | |
| 
							
							
								
								 | 
						f2b7f3a3b4 | |
| 
							
							
								
								 | 
						f9c9f6efbe | |
| 
							
							
								
								 | 
						0873b2da22 | |
| 
							
							
								
								 | 
						734a3e51c9 | |
| 
							
							
								
								 | 
						cf1a5cc922 | |
| 
							
							
								
								 | 
						6e503ed38f | |
| 
							
							
								
								 | 
						4cbff6b077 | |
| 
							
							
								
								 | 
						2b42dad617 | |
| 
							
							
								
								 | 
						8b6c400689 | |
| 
							
							
								
									
								
								 | 
						48972f84e1 | 
							
								
								
									
										31
									
								
								.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'.
 | 
				
			||||||
    'prettier', //'plugin:prettier/recommended'
 | 
					    'plugin:prettier/recommended',
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
| 
						 | 
					@ -54,10 +54,6 @@ 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: {
 | 
				
			||||||
| 
						 | 
					@ -70,19 +66,26 @@ 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
 | 
				
			||||||
  rules: {
 | 
					  rules: {
 | 
				
			||||||
    'prefer-promise-reject-errors': 'off',
 | 
					    // VueStuff
 | 
				
			||||||
 | 
					    // Defaults to error on eslint-plugin-vue 8.0.3, but let us be not too strict with names
 | 
				
			||||||
 | 
					    'vue/multi-word-component-names': 'off',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TypeScript
 | 
					    // Rejects on promises should always be of the Error type (and allow empty rejects as well)
 | 
				
			||||||
    quotes: ['warn', 'single', { avoidEscape: 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 }],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 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',
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ node_modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# We use yarn, so ignore npm
 | 
					# We use yarn, so ignore npm
 | 
				
			||||||
package-lock.json
 | 
					package-lock.json
 | 
				
			||||||
 | 
					yarn.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Quasar core related directories
 | 
					# Quasar core related directories
 | 
				
			||||||
.quasar
 | 
					.quasar
 | 
				
			||||||
| 
						 | 
					@ -17,6 +18,8 @@ package-lock.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# 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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +0,0 @@
 | 
				
			||||||
[submodule "deps/quasar-ui-qcalendar"]
 | 
					 | 
				
			||||||
	path = deps/quasar-ui-qcalendar
 | 
					 | 
				
			||||||
	url = https://github.com/susnux/quasar-ui-qcalendar
 | 
					 | 
				
			||||||
	branch = quasar2
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					yarn-error.log
 | 
				
			||||||
 | 
					.woodpecker/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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'),
 | 
				
			||||||
  ]
 | 
					  ],
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					pipeline:
 | 
				
			||||||
 | 
					  deploy:
 | 
				
			||||||
 | 
					    when:
 | 
				
			||||||
 | 
					      event: tag
 | 
				
			||||||
 | 
					      tag: "@flaschengeist/api-v*"
 | 
				
			||||||
 | 
					    image: node:lts-alpine
 | 
				
			||||||
 | 
					    commands:
 | 
				
			||||||
 | 
					      - cd api
 | 
				
			||||||
 | 
					      - echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > .npmrc
 | 
				
			||||||
 | 
					      - yarn publish --non-interactive
 | 
				
			||||||
 | 
					    secrets: [ node_auth_token ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					depends_on:
 | 
				
			||||||
 | 
					  - lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					pipeline:
 | 
				
			||||||
 | 
					  lint:
 | 
				
			||||||
 | 
					    when:
 | 
				
			||||||
 | 
					      branch: [main, develop]
 | 
				
			||||||
 | 
					    image: node:lts-alpine
 | 
				
			||||||
 | 
					    commands:
 | 
				
			||||||
 | 
					      - yarn install
 | 
				
			||||||
 | 
					      - yarn lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										106
									
								
								README.md
								
								
								
								
							
							
						
						| 
						 | 
					@ -1,62 +1,88 @@
 | 
				
			||||||
# 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					 "engines": {
 | 
				
			||||||
 | 
					    "node": ">= 14.18.1",
 | 
				
			||||||
 | 
					    "npm": ">= 6.14.12",
 | 
				
			||||||
 | 
					    "yarn": ">= 1.22.0"
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					So on debian (buster and bullseye) you will need to install node.js and yarn beside the debian packages to meet the needed versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pushd ~/opt
 | 
				
			||||||
 | 
					wget https://nodejs.org/dist/latest-v16.x/node-v16.13.0-linux-x64.tar.xz
 | 
				
			||||||
 | 
					tar -xJf node-v16.13.0-linux-x64.tar.xz
 | 
				
			||||||
 | 
					export PATH="$(pwd)/node-v16.13.0-linux-x64/bin":"$PATH"
 | 
				
			||||||
 | 
					npm i -g yarn
 | 
				
			||||||
 | 
					npm i -g @quasar/cli
 | 
				
			||||||
 | 
					popd
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Install the dependencies
 | 
					### Install the dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
yarn install
 | 
					yarn install
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Be aware npm might not work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Configure Plugins
 | 
					### Configure Plugins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can activate and deactive Plugins in `src/boot/plugins.ts`.
 | 
					#### Installing a plugin
 | 
				
			||||||
You have to set the name of the Plugin into `config.loadModules`.
 | 
					
 | 
				
			||||||
 | 
					Simply add it as a dependency and install it, for example installing the `pricelist`-plugin:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					yarn add '@flaschengeist/pricelist'
 | 
				
			||||||
 | 
					yarn install
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Enable / Disable a plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					After installing a plugin you will have to enable it,
 | 
				
			||||||
 | 
					this is done by adding it to the `plugin.config.js` file.
 | 
				
			||||||
 | 
					For the example above the file should look like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					module.exports = [
 | 
				
			||||||
 | 
					  // pricelist plugin:
 | 
				
			||||||
 | 
					  '@flaschengeist/pricelist',
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Remember to rebuild the project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Configure Backend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The application is using the API of [the backend](https://flaschengeist.dev/Flaschengeist/flaschengeist)
 | 
				
			||||||
 | 
					This access needs to be configured in `src/config.ts'->config.baseURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- either you do have a proxy webserver that maps the '/api' to the backend (http://localhost:5000) or
 | 
				
			||||||
 | 
					- you do directly configure the backend there:`baseURL: 'http://localhost:5000'`. Be aware not committing this configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Build the application
 | 
					### Build the application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```sh
 | 
				
			||||||
yarn quasar build
 | 
					yarn quasar build
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Notes on mobile apps (Cordova)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For mobile applications older web engines should or must be supported,
 | 
				
			||||||
 | 
					as manufaturer often do not update their phones, so for building cordova apps set the `BROWSERSLIST_ENV` environment variable to
 | 
				
			||||||
 | 
					`BROWSERSLIST_ENV=cordova`.
 | 
				
			||||||
 | 
					This will produce ECDMAscript compatible with iOS 13+ and Android Webview 76 (relased October 2019).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Icons used
 | 
					Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
 | 
				
			||||||
 | 
					 | 
				
			||||||
We are using the `mdi-v5` icon set, so feel free to use any icon from it.
 | 
					 | 
				
			||||||
A list can be found [here](https://materialdesignicons.com/)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Commands useful for development
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Start the app in development mode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Provides hot-code reloading, error reporting, etc.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
yarn quasar dev
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### File linting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
yarn run lint
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Plugins
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Build a Plugin
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
A Flaschengeist-Frontend-Plugin should be placed in `src/plugins`.
 | 
					 | 
				
			||||||
It needs a `plugin.ts` File which exports a plugin with the following interface:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
name: string;
 | 
					 | 
				
			||||||
mainRoutes?: PluginRouteConfig[];
 | 
					 | 
				
			||||||
outRoutes?: PluginRouteConfig[];
 | 
					 | 
				
			||||||
requiredModules: string[];
 | 
					 | 
				
			||||||
version: string;
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You have to import `FG_Plugin` from `plugins.d.ts`.
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, PropType } from 'vue';
 | 
					import { computed, defineComponent, PropType } from 'vue';
 | 
				
			||||||
import { date as q_date } from 'quasar';
 | 
					import { date as q_date } from 'quasar';
 | 
				
			||||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from 'src/utils/validators';
 | 
					import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'IsoDateInput',
 | 
					  name: 'IsoDateInput',
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@ export default defineComponent({
 | 
				
			||||||
    label: { type: String, default: 'Datum' },
 | 
					    label: { type: String, default: 'Datum' },
 | 
				
			||||||
    readonly: Boolean,
 | 
					    readonly: Boolean,
 | 
				
			||||||
    rules: {
 | 
					    rules: {
 | 
				
			||||||
      type: Array as PropType<Validator[]>,
 | 
					      type: Array as PropType<Validator<Date>[]>,
 | 
				
			||||||
      default: () => [],
 | 
					      default: () => [],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,22 @@ export default defineComponent({
 | 
				
			||||||
  setup(props, { emit, attrs }) {
 | 
					  setup(props, { emit, attrs }) {
 | 
				
			||||||
    const customRules = computed(() => [
 | 
					    const customRules = computed(() => [
 | 
				
			||||||
      props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
 | 
					      props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
 | 
				
			||||||
      ...props.rules,
 | 
					      (value?: string) => {
 | 
				
			||||||
 | 
					        if (props.rules.length > 0 && !!value) {
 | 
				
			||||||
 | 
					          let date: Date | undefined = undefined;
 | 
				
			||||||
 | 
					          if (props.type == 'date') date = modifyDate(value);
 | 
				
			||||||
 | 
					          else if (props.type == 'time') date = modifyTime(value);
 | 
				
			||||||
 | 
					          else {
 | 
				
			||||||
 | 
					            const split = value.split(' ');
 | 
				
			||||||
 | 
					            date = modifyTime(split[1], modifyDate(split[0]));
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          for (const rule of props.rules) {
 | 
				
			||||||
 | 
					            const r = rule(date);
 | 
				
			||||||
 | 
					            if (typeof r === 'string') return r;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const clearable = computed(() =>
 | 
					    const clearable = computed(() =>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-avatar>
 | 
				
			||||||
 | 
					    <slot :avatar-u-r-l="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>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					import IsoDateInput from './IsoDateInput.vue';
 | 
				
			||||||
 | 
					import PasswordInput from './PasswordInput.vue';
 | 
				
			||||||
 | 
					import UserAvatar from './UserAvatar.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { IsoDateInput, PasswordInput, UserAvatar };
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					export { api, pinia } from './src/internal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from './src/stores/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from './src/utils/datetime';
 | 
				
			||||||
 | 
					export * from './src/utils/permission';
 | 
				
			||||||
 | 
					export * from './src/utils/persistent';
 | 
				
			||||||
 | 
					export * from './src/utils/user';
 | 
				
			||||||
 | 
					export * from './src/utils/validators';
 | 
				
			||||||
 | 
					export * from './src/utils/misc';
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "version": "1.0.0",
 | 
				
			||||||
 | 
					  "name": "@flaschengeist/api",
 | 
				
			||||||
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
 | 
					  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
				
			||||||
 | 
					  "description": "Modular student club administration system",
 | 
				
			||||||
 | 
					  "bugs": {
 | 
				
			||||||
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "main": "./src/index.ts",
 | 
				
			||||||
 | 
					  "peerDependencies": {
 | 
				
			||||||
 | 
					    "@quasar/app-webpack": "^3.7.2",
 | 
				
			||||||
 | 
					    "flaschengeist": "^2.0.0",
 | 
				
			||||||
 | 
					    "pinia": "^2.0.8"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@flaschengeist/types": "^1.0.0",
 | 
				
			||||||
 | 
					    "@types/node": "^14.18.0",
 | 
				
			||||||
 | 
					    "typescript": "^4.5.4"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "prettier": {
 | 
				
			||||||
 | 
					    "singleQuote": true,
 | 
				
			||||||
 | 
					    "semi": true,
 | 
				
			||||||
 | 
					    "printWidth": 100,
 | 
				
			||||||
 | 
					    "arrowParens": "always"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					//https://github.com/vuejs/vue-next/issues/3130
 | 
				
			||||||
 | 
					declare module '*.vue' {
 | 
				
			||||||
 | 
					  import { ComponentOptions } from 'vue';
 | 
				
			||||||
 | 
					  const component: ComponentOptions;
 | 
				
			||||||
 | 
					  export default component;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { createPinia } from 'pinia';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const api = axios.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const pinia = createPinia();
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import { AxiosError } from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if error is an AxiosError, and optional if a specific status was returned
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param error Thrown error to check
 | 
				
			||||||
 | 
					 * @param status If set, check if this error has set thouse status code
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function isAxiosError(error: unknown, status?: number) {
 | 
				
			||||||
 | 
					  // Check if it is an axios error (with axios 1.0 `error instanceof AxiosError` will be possible)
 | 
				
			||||||
 | 
					  if (typeof error !== 'object' || !error || !('isAxiosError' in error)) return false;
 | 
				
			||||||
 | 
					  // Check status code if status was given
 | 
				
			||||||
 | 
					  if (status !== undefined)
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      (<AxiosError>error).response !== undefined && (<AxiosError>error).response?.status === status
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from './main';
 | 
				
			||||||
 | 
					export * from './session';
 | 
				
			||||||
 | 
					export * from './user';
 | 
				
			||||||
| 
						 | 
					@ -1,42 +1,48 @@
 | 
				
			||||||
import { useUserStore, useSessionStore } from 'src/plugins/user/store';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import { translateNotification } from 'src/boot/plugins';
 | 
					import { fixSession, useSessionStore, useUserStore } from '.';
 | 
				
			||||||
import { LocalStorage, SessionStorage } from 'quasar';
 | 
					 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					import { PersistentStorage } from '../utils/persistent';
 | 
				
			||||||
function loadCurrentSession() {
 | 
					import { LocalStorage, SessionStorage } from 'quasar';
 | 
				
			||||||
  const session = LocalStorage.getItem<FG.Session>('session');
 | 
					function reviveSession() {
 | 
				
			||||||
  if (session) session.expires = new Date(session.expires);
 | 
					  return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
 | 
				
			||||||
  return session || undefined;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function loadUser() {
 | 
					function clearPersistant() {
 | 
				
			||||||
  const user = SessionStorage.getItem<FG.User>('user');
 | 
					  void PersistentStorage.remove('fg_session');
 | 
				
			||||||
  if (user && user.birthday) user.birthday = new Date(user.birthday);
 | 
					}
 | 
				
			||||||
  return user || undefined;
 | 
					
 | 
				
			||||||
 | 
					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',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state: () => ({
 | 
					  state: () => ({
 | 
				
			||||||
    session: loadCurrentSession(),
 | 
					    session: undefined as FG.Session | undefined,
 | 
				
			||||||
    user: loadUser(),
 | 
					    user: undefined as FG.User | undefined,
 | 
				
			||||||
    notifications: [] as Array<FG_Plugin.Notification>,
 | 
					    notifications: [] as Array<FG_Plugin.Notification>,
 | 
				
			||||||
    shortcuts: [] as FG_Plugin.MenuLink[],
 | 
					    shortcuts: [] as Array<FG_Plugin.MenuLink>,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getters: {
 | 
					  getters: {
 | 
				
			||||||
    loggedIn() {
 | 
					    loggedIn(): boolean {
 | 
				
			||||||
      return this.session !== undefined;
 | 
					      return this.session !== undefined;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    currentUser() {
 | 
					    currentUser(): FG.User {
 | 
				
			||||||
      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;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    permissions() {
 | 
					    currentSession(): FG.Session {
 | 
				
			||||||
 | 
					      if (this.session === undefined) throw 'Not logged in, this should not be called';
 | 
				
			||||||
 | 
					      return this.session;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    permissions(): string[] {
 | 
				
			||||||
      return this.user?.permissions || [];
 | 
					      return this.user?.permissions || [];
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -46,29 +52,29 @@ export const useMainStore = defineStore({
 | 
				
			||||||
     *  Updates session and loads current user
 | 
					     *  Updates session and loads current user
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    async init() {
 | 
					    async init() {
 | 
				
			||||||
      if (this.session) {
 | 
					 | 
				
			||||||
      const sessionStore = useSessionStore();
 | 
					      const sessionStore = useSessionStore();
 | 
				
			||||||
        const session = await sessionStore.getSession(this.session.token);
 | 
					 | 
				
			||||||
        if (session) {
 | 
					 | 
				
			||||||
          this.session = this.session;
 | 
					 | 
				
			||||||
      const userStore = useUserStore();
 | 
					      const userStore = useUserStore();
 | 
				
			||||||
          const user = await userStore.getUser(this.session.userid);
 | 
					
 | 
				
			||||||
          if (user) {
 | 
					      try {
 | 
				
			||||||
            this.user = user;
 | 
					        this.session = await reviveSession();
 | 
				
			||||||
            SessionStorage.set('user', user);
 | 
					        if (this.session !== undefined) {
 | 
				
			||||||
          }
 | 
					          this.session = await sessionStore.getSession(this.session.token);
 | 
				
			||||||
 | 
					          if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      } 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 = data;
 | 
					        this.session = fixSession(data);
 | 
				
			||||||
        this.session.expires = new Date(this.session.expires);
 | 
					        this.user = await userStore.getUser(data.userid, true);
 | 
				
			||||||
        LocalStorage.set('session', this.session);
 | 
					 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      } catch ({ response }) {
 | 
					      } catch ({ response }) {
 | 
				
			||||||
 | 
					        this.handleLoggedOut();
 | 
				
			||||||
        return (<AxiosResponse | undefined>response)?.status || false;
 | 
					        return (<AxiosResponse | undefined>response)?.status || false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -76,19 +82,13 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    async logout() {
 | 
					    async logout() {
 | 
				
			||||||
      if (!this.session || !this.session.token) return false;
 | 
					      if (!this.session || !this.session.token) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      LocalStorage.clear();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const token = this.session.token;
 | 
					        const token = this.session.token;
 | 
				
			||||||
        this.$patch({
 | 
					 | 
				
			||||||
          session: undefined,
 | 
					 | 
				
			||||||
          user: undefined,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        await api.delete(`/auth/${token}`);
 | 
					        await api.delete(`/auth/${token}`);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
        SessionStorage.clear();
 | 
					        this.handleLoggedOut();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -110,23 +110,22 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
 | 
					    async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
 | 
				
			||||||
      const params =
 | 
					      const { data } = await api.get<FG.Notification[]>('/notifications', {
 | 
				
			||||||
 | 
					        params:
 | 
				
			||||||
          this.notifications.length > 0
 | 
					          this.notifications.length > 0
 | 
				
			||||||
            ? { from: this.notifications[this.notifications.length - 1].time }
 | 
					            ? { from: this.notifications[this.notifications.length - 1].time }
 | 
				
			||||||
          : {};
 | 
					            : {},
 | 
				
			||||||
      const { data } = await api.get<FG.Notification[]>('/notifications', { params: params });
 | 
					      });
 | 
				
			||||||
      const notifications: FG_Plugin.Notification[] = [];
 | 
					
 | 
				
			||||||
 | 
					      const notes = [] as FG_Plugin.Notification[];
 | 
				
			||||||
      data.forEach((n) => {
 | 
					      data.forEach((n) => {
 | 
				
			||||||
        n.time = new Date(n.time);
 | 
					        n.time = new Date(n.time);
 | 
				
			||||||
        notifications.push(
 | 
					        const plugin = flaschengeist?.plugins.filter((p) => p.id === n.plugin)[0];
 | 
				
			||||||
          (
 | 
					        if (!plugin) console.debug('Could not find a parser for this notification', n);
 | 
				
			||||||
            flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification ||
 | 
					        else notes.push(plugin.notification(n));
 | 
				
			||||||
            translateNotification
 | 
					 | 
				
			||||||
          )(n)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      this.notifications.push(...notifications);
 | 
					      this.notifications.push(...notes);
 | 
				
			||||||
      return notifications;
 | 
					      return notes;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async removeNotification(id: number) {
 | 
					    async removeNotification(id: number) {
 | 
				
			||||||
| 
						 | 
					@ -151,6 +150,13 @@ 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() {
 | 
				
			||||||
 | 
					      this.$reset();
 | 
				
			||||||
 | 
					      void clearPersistant();
 | 
				
			||||||
 | 
					      LocalStorage.clear();
 | 
				
			||||||
 | 
					      SessionStorage.clear();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					import { api } from '../internal';
 | 
				
			||||||
 | 
					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({
 | 
				
			||||||
 | 
					  id: 'sessions',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state: () => ({}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getters: {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  actions: {
 | 
				
			||||||
 | 
					    async getSession(token: string) {
 | 
				
			||||||
 | 
					      return await api
 | 
				
			||||||
 | 
					        .get(`/auth/${token}`)
 | 
				
			||||||
 | 
					        .then(({ data }: AxiosResponse<FG.Session>) => data)
 | 
				
			||||||
 | 
					        .catch(() => undefined);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getSessions() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const { data } = await api.get<FG.Session[]>('/auth');
 | 
				
			||||||
 | 
					        data.forEach(fixSession);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
 | 
					        const currentSession = data.find((session) => {
 | 
				
			||||||
 | 
					          return session.token === mainStore.session?.token;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (currentSession) {
 | 
				
			||||||
 | 
					          mainStore.session = currentSession;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        return [] as FG.Session[];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteSession(token: string) {
 | 
				
			||||||
 | 
					      const mainStore = useMainStore();
 | 
				
			||||||
 | 
					      if (token === mainStore.session?.token) return mainStore.logout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await api.delete(`/auth/${token}`);
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        // Ignore 401, as this means we are already logged out, throw all other
 | 
				
			||||||
 | 
					        if (!isAxiosError(error, 401)) throw error;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async updateSession(lifetime: number, token: string) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
				
			||||||
 | 
					        fixSession(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
 | 
					        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,278 @@
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					import { api } from '../internal';
 | 
				
			||||||
 | 
					import { isAxiosError, useMainStore } from '.';
 | 
				
			||||||
 | 
					import { DisplayNameMode } from '@flaschengeist/users';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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({
 | 
				
			||||||
 | 
					  id: 'users',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state: () => ({
 | 
				
			||||||
 | 
					    roles: [] as FG.Role[],
 | 
				
			||||||
 | 
					    permissions: [] as FG.Permission[],
 | 
				
			||||||
 | 
					    userSettings: {} as FG.UserSettings,
 | 
				
			||||||
 | 
					    // list of all users, include deleted ones, use `users` getter for list of active ones
 | 
				
			||||||
 | 
					    _users: [] as FG.User[],
 | 
				
			||||||
 | 
					    // Internal flags for deciding if lists need to force-loaded
 | 
				
			||||||
 | 
					    _dirty_users: 0,
 | 
				
			||||||
 | 
					    _dirty_roles: 0,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getters: {
 | 
				
			||||||
 | 
					    users(state) {
 | 
				
			||||||
 | 
					      const u = state._users.filter((u) => !u.deleted);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      switch (this.userSettings['display_name']) {
 | 
				
			||||||
 | 
					        case DisplayNameMode.FIRSTNAME_LASTNAME || DisplayNameMode.FIRSTNAME:
 | 
				
			||||||
 | 
					          u.sort((a, b) => {
 | 
				
			||||||
 | 
					            const a_lastname = a.lastname.toLowerCase();
 | 
				
			||||||
 | 
					            const b_lastname = b.lastname.toLowerCase();
 | 
				
			||||||
 | 
					            const a_firstname = a.firstname.toLowerCase();
 | 
				
			||||||
 | 
					            const b_firstname = b.firstname.toLowerCase();
 | 
				
			||||||
 | 
					            if (a_firstname === b_firstname) {
 | 
				
			||||||
 | 
					              return a_lastname < b_lastname ? -1 : 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return a_firstname < b_firstname ? -1 : 1;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case <string>DisplayNameMode.DISPLAYNAME:
 | 
				
			||||||
 | 
					          u.sort((a, b) => {
 | 
				
			||||||
 | 
					            const a_displayname = a.display_name.toLowerCase();
 | 
				
			||||||
 | 
					            const b_displayname = b.display_name.toLowerCase();
 | 
				
			||||||
 | 
					            return a_displayname < b_displayname ? -1 : 1;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          u.sort((a, b) => {
 | 
				
			||||||
 | 
					            const a_lastname = a.lastname.toLowerCase();
 | 
				
			||||||
 | 
					            const b_lastname = b.lastname.toLowerCase();
 | 
				
			||||||
 | 
					            const a_firstname = a.firstname.toLowerCase();
 | 
				
			||||||
 | 
					            const b_firstname = b.firstname.toLowerCase();
 | 
				
			||||||
 | 
					            if (a_lastname === b_lastname) {
 | 
				
			||||||
 | 
					              return a_firstname < b_firstname ? -1 : 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return a_lastname < b_lastname ? -1 : 1;
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return u;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  actions: {
 | 
				
			||||||
 | 
					    /** Simply filter all users by ID */
 | 
				
			||||||
 | 
					    findUser(userid: string) {
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					      const idx = this._users.findIndex((user) => user.userid === userid);
 | 
				
			||||||
 | 
					      if (force || idx === -1 || isDirty(this._dirty_users)) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const { data } = await api.get<FG.User>(`/users/${userid}`);
 | 
				
			||||||
 | 
					          fixUser(data);
 | 
				
			||||||
 | 
					          if (idx === -1) this._users.push(data);
 | 
				
			||||||
 | 
					          else this._users[idx] = data;
 | 
				
			||||||
 | 
					          return data;
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          // Ignore 404, throw all other
 | 
				
			||||||
 | 
					          if (!isAxiosError(error, 404)) throw error;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        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) {
 | 
				
			||||||
 | 
					      if (force || isDirty(this._dirty_users)) {
 | 
				
			||||||
 | 
					        const { data } = await api.get<FG.User[]>('/users');
 | 
				
			||||||
 | 
					        data.forEach(fixUser);
 | 
				
			||||||
 | 
					        this._users = data;
 | 
				
			||||||
 | 
					        this._dirty_users = Date.now();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					      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();
 | 
				
			||||||
 | 
					      if (user.userid === mainStore.user?.userid) mainStore.user = user;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 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) {
 | 
				
			||||||
 | 
					      const { data } = await api.post<FG.User>('/users', user);
 | 
				
			||||||
 | 
					      this._users.push(<FG.User>fixUser(data));
 | 
				
			||||||
 | 
					      return data;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Delete an user
 | 
				
			||||||
 | 
					     * 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();
 | 
				
			||||||
 | 
					      formData.append('file', file);
 | 
				
			||||||
 | 
					      await api.post(`/users/${user}/avatar`, formData, {
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Delete avatar of an user
 | 
				
			||||||
 | 
					     * @param user User or ID of user
 | 
				
			||||||
 | 
					     * @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) {
 | 
				
			||||||
 | 
					      if (force || this.permissions.length === 0) {
 | 
				
			||||||
 | 
					        const { data } = await api.get<FG.Permission[]>('/roles/permissions');
 | 
				
			||||||
 | 
					        this.permissions = data;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					      if (force || isDirty(this._dirty_roles)) {
 | 
				
			||||||
 | 
					        const { data } = await api.get<FG.Role[]>('/roles');
 | 
				
			||||||
 | 
					        this.roles = data;
 | 
				
			||||||
 | 
					        this._dirty_roles = Date.now();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					      await api.put(`/roles/${role.id}`, role);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const idx = this.roles.findIndex((r) => r.id === role.id);
 | 
				
			||||||
 | 
					      if (idx != -1) this.roles[idx] = role;
 | 
				
			||||||
 | 
					      else this._dirty_roles = 0;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 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) {
 | 
				
			||||||
 | 
					      const { data } = await api.post<FG.Role>('/roles', role);
 | 
				
			||||||
 | 
					      this.roles.push(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) {
 | 
				
			||||||
 | 
					      if (typeof role === 'object') role = role.id;
 | 
				
			||||||
 | 
					      await api.delete(`/roles/${role}`);
 | 
				
			||||||
 | 
					      this.roles = this.roles.filter((r) => r.id !== role);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Get Settings for display name mode
 | 
				
			||||||
 | 
					     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
				
			||||||
 | 
					     * @throws Probably an AxiosError if request failed
 | 
				
			||||||
 | 
					     * @returns Settings for display name mode
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async getDisplayNameModeSetting(force = false): Promise<string> {
 | 
				
			||||||
 | 
					      const mainStore = useMainStore();
 | 
				
			||||||
 | 
					      if (force) {
 | 
				
			||||||
 | 
					        const { data } = await api.get<{ data: string }>(
 | 
				
			||||||
 | 
					          `users/${mainStore.currentUser.userid}/setting/display_name_mode`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        this.userSettings['display_name'] = data.data;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return this.userSettings['display_name'];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Set Settings for display name mode
 | 
				
			||||||
 | 
					     * @param mode New display name mode
 | 
				
			||||||
 | 
					     * @throws Probably an AxiosError if request failed
 | 
				
			||||||
 | 
					     * @returns Settings for display name mode
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async setDisplayNameModeSetting(mode: string): Promise<string> {
 | 
				
			||||||
 | 
					      const mainStore = useMainStore();
 | 
				
			||||||
 | 
					      await api.put(`users/${mainStore.currentUser.userid}/setting/display_name_mode`, {
 | 
				
			||||||
 | 
					        data: mode,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.userSettings['display_name'] = mode;
 | 
				
			||||||
 | 
					      return mode;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -17,12 +17,23 @@ export function formatDateTime(
 | 
				
			||||||
  return dateTimeFormat.format(date);
 | 
					  return dateTimeFormat.format(date);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function asDate(date?: Date) {
 | 
					export function asDate(date?: Date, placeholder = '') {
 | 
				
			||||||
  return date ? formatDateTime(date, true) : '';
 | 
					  return date ? formatDateTime(date, true) : placeholder;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function asHour(date?: Date) {
 | 
					export function asHour(date?: Date, placeholder = '') {
 | 
				
			||||||
  return date ? formatDateTime(date, false, true) : '';
 | 
					  return date ? formatDateTime(date, false, true) : placeholder;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function formatStartEnd(start: Date, end?: Date) {
 | 
				
			||||||
 | 
					  const today = asDate(new Date());
 | 
				
			||||||
 | 
					  const startDate = asDate(start);
 | 
				
			||||||
 | 
					  const endDate = end ? asDate(end) : '';
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    (today !== startDate ? `${startDate}, ` : '') +
 | 
				
			||||||
 | 
					    asHour(start) +
 | 
				
			||||||
 | 
					    (end ? ' - ' + (endDate !== startDate ? `${endDate}, ` : '') + asHour(end) : '')
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function startOfWeek(date: Date, startMonday = true) {
 | 
					export function startOfWeek(date: Date, startMonday = true) {
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export function clone<T>(o: T): T {
 | 
				
			||||||
 | 
					  return <T>JSON.parse(JSON.stringify(o));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					import { useMainStore } from '../stores';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function hasPermission(permission: string) {
 | 
					export function hasPermission(permission: string) {
 | 
				
			||||||
  const store = useMainStore();
 | 
					  const store = useMainStore();
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import { LocalStorage, Platform } from 'quasar';
 | 
				
			||||||
 | 
					import { Preferences } from '@capacitor/preferences';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 Preferences.clear();
 | 
				
			||||||
 | 
					    else return Promise.resolve(LocalStorage.clear());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static remove(key: string) {
 | 
				
			||||||
 | 
					    if (Platform.is.capacitor) return Preferences.remove({ key: key });
 | 
				
			||||||
 | 
					    else return Promise.resolve(LocalStorage.remove(key));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static set(key: string, value: PersitentTypes) {
 | 
				
			||||||
 | 
					    if (Platform.is.capacitor) return Preferences.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 Preferences.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 Preferences.keys().then((v) => v.keys);
 | 
				
			||||||
 | 
					    else return Promise.resolve(LocalStorage.getAllKeys());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					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,4 @@
 | 
				
			||||||
export type Validator = (value: unknown) => boolean | string;
 | 
					export type Validator<T = unknown> = (value?: T | null) => boolean | string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function notEmpty(val: unknown) {
 | 
					export function notEmpty(val: unknown) {
 | 
				
			||||||
  return !!val || 'Feld darf nicht leer sein!';
 | 
					  return !!val || 'Feld darf nicht leer sein!';
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "@quasar/app/tsconfig-preset",
 | 
				
			||||||
 | 
					  "target": "esnext",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "baseUrl": "./",
 | 
				
			||||||
 | 
					    "lib": ["es2020", "dom"],
 | 
				
			||||||
 | 
					    "types": ["@flaschengeist/types", "@quasar/app", "node"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,4 @@
 | 
				
			||||||
/* eslint-env node */
 | 
					/* eslint-env node */
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  presets: [
 | 
					  presets: ['@quasar/babel-preset-app'],
 | 
				
			||||||
    '@quasar/babel-preset-app'
 | 
					};
 | 
				
			||||||
  ]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
Subproject commit f245cb8b16c855c059d9170611797028c600696a
 | 
					 | 
				
			||||||
							
								
								
									
										89
									
								
								package.json
								
								
								
								
							
							
						
						| 
						 | 
					@ -1,43 +1,52 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
  "version": "2.0.0-alpha.1",
 | 
					  "version": "2.1.0",
 | 
				
			||||||
  "productName": "Flaschengeist",
 | 
					  "productName": "flaschengeist-frontend",
 | 
				
			||||||
  "name": "flaschengeist-frontend",
 | 
					  "name": "flaschengeist",
 | 
				
			||||||
  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
					  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
				
			||||||
  "description": "Modular student club administration system",
 | 
					  "description": "Modular student club administration system",
 | 
				
			||||||
  "bugs": {
 | 
					  "bugs": {
 | 
				
			||||||
    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
					    "format": "prettier --config ./package.json  --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
 | 
				
			||||||
    "lint": "eslint --ext .js,.ts,.vue ./src"
 | 
					    "lint": "eslint --ext .js,.ts,.vue ./src ./api"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "axios": "^0.21.1",
 | 
					    "@flaschengeist/api": "^1.0.0",
 | 
				
			||||||
    "cordova": "^10.0.0",
 | 
					    "@flaschengeist/balance": "^1.0.0",
 | 
				
			||||||
    "pinia": "^2.0.0-alpha.10",
 | 
					    "@flaschengeist/pricelist-old": "^1.0.0",
 | 
				
			||||||
    "quasar": "^2.0.0-beta.12",
 | 
					    "@flaschengeist/schedule": "^1.0.0",
 | 
				
			||||||
    "vuedraggable": "^4.0.1"
 | 
					    "@flaschengeist/users": "^1.0.0",
 | 
				
			||||||
 | 
					    "axios": "^1.4.0",
 | 
				
			||||||
 | 
					    "pinia": "^2.0.8",
 | 
				
			||||||
 | 
					    "quasar": "^2.11.10",
 | 
				
			||||||
 | 
					    "vue": "^3.0.0",
 | 
				
			||||||
 | 
					    "vue-router": "^4.0.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@quasar/app": "^3.0.0-beta.13",
 | 
					    "@capacitor/core": "^5.0.0",
 | 
				
			||||||
    "@quasar/extras": "^1.10.2",
 | 
					    "@capacitor/preferences": "^5.0.0",
 | 
				
			||||||
    "@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
 | 
					    "@flaschengeist/types": "^1.0.0",
 | 
				
			||||||
    "@types/node": "^12.20.7",
 | 
					    "@quasar/app-webpack": "^3.7.2",
 | 
				
			||||||
    "@types/webpack": "^4.41.27",
 | 
					    "@quasar/extras": "^1.16.3",
 | 
				
			||||||
    "@types/webpack-env": "^1.16.0",
 | 
					    "@types/node": "^14.18.0",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^4.20.0",
 | 
					    "@types/webpack": "^5.28.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^4.20.0",
 | 
					    "@types/webpack-env": "^1.16.3",
 | 
				
			||||||
    "electron": "^12.0.4",
 | 
					    "@typescript-eslint/eslint-plugin": "^5.8.0",
 | 
				
			||||||
    "electron-packager": "^14.1.1",
 | 
					    "@typescript-eslint/parser": "^5.8.0",
 | 
				
			||||||
    "eslint": "^7.23.0",
 | 
					    "@vue/devtools": "^6.5.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.1.0",
 | 
					    "eslint": "^8.5.0",
 | 
				
			||||||
    "eslint-plugin-vue": "^7.8.0",
 | 
					    "eslint-config-prettier": "^8.3.0",
 | 
				
			||||||
    "eslint-webpack-plugin": "^2.5.3",
 | 
					    "eslint-plugin-prettier": "^4.0.0",
 | 
				
			||||||
    "prettier": "^2.2.1",
 | 
					    "eslint-plugin-vue": "^9.14.1",
 | 
				
			||||||
    "typescript": "^4.2.3"
 | 
					    "eslint-webpack-plugin": "^4.0.1",
 | 
				
			||||||
 | 
					    "modify-source-webpack-plugin": "^4.1.0",
 | 
				
			||||||
 | 
					    "prettier": "^2.5.1",
 | 
				
			||||||
 | 
					    "typescript": "^4.5.4",
 | 
				
			||||||
 | 
					    "vuedraggable": "^4.1.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "prettier": {
 | 
					  "prettier": {
 | 
				
			||||||
    "singleQuote": true,
 | 
					    "singleQuote": true,
 | 
				
			||||||
| 
						 | 
					@ -45,19 +54,25 @@
 | 
				
			||||||
    "printWidth": 100,
 | 
					    "printWidth": 100,
 | 
				
			||||||
    "arrowParens": "always"
 | 
					    "arrowParens": "always"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "browserslist": [
 | 
					  "browserslist": {
 | 
				
			||||||
    "last 10 Chrome versions",
 | 
					    "defaults": [
 | 
				
			||||||
    "last 10 Firefox versions",
 | 
					      "Firefox esr",
 | 
				
			||||||
 | 
					      "last 6 Chrome versions",
 | 
				
			||||||
 | 
					      "last 4 Firefox versions",
 | 
				
			||||||
      "last 4 Edge versions",
 | 
					      "last 4 Edge versions",
 | 
				
			||||||
      "last 4 Safari versions",
 | 
					      "last 4 Safari versions",
 | 
				
			||||||
    "last 8 Android versions",
 | 
					      "last 4 ChromeAndroid versions",
 | 
				
			||||||
    "last 1 ChromeAndroid versions",
 | 
					      "last 1 FirefoxAndroid versions"
 | 
				
			||||||
    "last 1 FirefoxAndroid versions",
 | 
					 | 
				
			||||||
    "last 6 iOS versions"
 | 
					 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    "cordova": [
 | 
				
			||||||
 | 
					      "iOS >= 13.0",
 | 
				
			||||||
 | 
					      "Android >= 76",
 | 
				
			||||||
 | 
					      "ChromeAndroid >= 76"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
    "node": ">= 12.0.0",
 | 
					    "node": ">= 14.18.1",
 | 
				
			||||||
    "npm": ">= 6.13.4",
 | 
					    "npm": ">= 6.14.12",
 | 
				
			||||||
    "yarn": ">= 1.21.1"
 | 
					    "yarn": ">= 1.22.0"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					// You can add your plugins here
 | 
				
			||||||
 | 
					module.exports = [
 | 
				
			||||||
 | 
					//  '@flaschengeist/balance',
 | 
				
			||||||
 | 
					//  '@flaschengeist/schedule',
 | 
				
			||||||
 | 
					//  '@flaschengeist/pricelist',
 | 
				
			||||||
 | 
					  '@flaschengeist/schedule',
 | 
				
			||||||
							
								
								
									
										109
									
								
								quasar.conf.js
								
								
								
								
							
							
						
						| 
						 | 
					@ -8,12 +8,26 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 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, ReplaceOperation } = require('modify-source-webpack-plugin');
 | 
				
			||||||
const { configure } = require('quasar/wrappers');
 | 
					const { configure } = require('quasar/wrappers');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const operation = () => {
 | 
				
			||||||
 | 
					  const custom_plgns = require('./plugin.config.js');
 | 
				
			||||||
 | 
					  const required_plgns = require('./src/vendor-plugin.config.js');
 | 
				
			||||||
 | 
					  const plugins = [...custom_plgns, ...required_plgns].map(
 | 
				
			||||||
 | 
					    (v) => `import("${v}").catch(() => "${v}")`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const replace = new ReplaceOperation(
 | 
				
			||||||
 | 
					    'all',
 | 
				
			||||||
 | 
					    `\\/\\* *INSERT_PLUGIN_LIST *\\*\\/`,
 | 
				
			||||||
 | 
					    `${plugins.join(', ')}`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  return replace;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = configure(function (/* ctx */) {
 | 
					module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/supporting-ts
 | 
					 | 
				
			||||||
    // https://quasar.dev/quasar-cli/supporting-ts
 | 
					    // https://quasar.dev/quasar-cli/supporting-ts
 | 
				
			||||||
    supportTS: {
 | 
					    supportTS: {
 | 
				
			||||||
      tsCheckerConfig: {
 | 
					      tsCheckerConfig: {
 | 
				
			||||||
| 
						 | 
					@ -21,7 +35,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 +44,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', 'loading', 'login'],
 | 
					    boot: ['axios', 'store', 'plugins', 'login', 'init'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 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'],
 | 
				
			||||||
| 
						 | 
					@ -39,10 +53,10 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
    extras: [
 | 
					    extras: [
 | 
				
			||||||
      // 'eva-icons',
 | 
					      // 'eva-icons',
 | 
				
			||||||
      // 'fontawesome-v5',
 | 
					      // 'fontawesome-v5',
 | 
				
			||||||
      // 'ionicons-v4',
 | 
					      // 'ionicons-v5',
 | 
				
			||||||
      // 'line-awesome',
 | 
					      // 'line-awesome',
 | 
				
			||||||
      // 'material-icons',
 | 
					      // 'material-icons',
 | 
				
			||||||
      'mdi-v5',
 | 
					      'mdi-v7',
 | 
				
			||||||
      // 'themify',
 | 
					      // 'themify',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 | 
					      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 | 
				
			||||||
| 
						 | 
					@ -52,7 +66,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
 | 
					    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
 | 
				
			||||||
    build: {
 | 
					    build: {
 | 
				
			||||||
      vueRouterMode: 'history', // available values: 'hash', 'history'
 | 
					      vueRouterMode: 'history', // available values: 'hash', 'history'
 | 
				
			||||||
 | 
					      //publicPath: 'flaschengeist2',
 | 
				
			||||||
      // transpile: false,
 | 
					      // transpile: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
					      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
				
			||||||
| 
						 | 
					@ -60,10 +74,8 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      // Applies only if "transpile" is set to true.
 | 
					      // Applies only if "transpile" is set to true.
 | 
				
			||||||
      // transpileDependencies: [],
 | 
					      // transpileDependencies: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // rtl: false, // https://quasar.dev/options/rtl-support
 | 
					      // rtl: false,
 | 
				
			||||||
      // preloadChunks: true,
 | 
					
 | 
				
			||||||
      // showProgress: false,
 | 
					 | 
				
			||||||
      // gzip: true,
 | 
					 | 
				
			||||||
      // analyze: true,
 | 
					      // analyze: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Options below are automatically set depending on the env, set them if you want to override
 | 
					      // Options below are automatically set depending on the env, set them if you want to override
 | 
				
			||||||
| 
						 | 
					@ -71,34 +83,50 @@ 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')
 | 
					        chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
 | 
				
			||||||
          .use(ESLintPlugin, [{
 | 
					          {
 | 
				
			||||||
            extensions: [ 'ts', 'js', 'vue' ],
 | 
					            extensions: ['ts', 'js', 'vue'],
 | 
				
			||||||
            exclude: 'node_modules'
 | 
					            exclude: ['node_modules', 'src-capacitor'],
 | 
				
			||||||
          }])
 | 
					          },
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            rules: [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                test: /plugins\.ts$/,
 | 
				
			||||||
 | 
					                operations: [operation()],
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					        chain.merge({
 | 
				
			||||||
 | 
					          snapshot: {
 | 
				
			||||||
 | 
					            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
 | 
				
			||||||
    devServer: {
 | 
					    devServer: {
 | 
				
			||||||
      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/**/*'] },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
 | 
					    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
 | 
				
			||||||
    framework: {
 | 
					    framework: {
 | 
				
			||||||
      iconSet: 'mdi-v5', // Quasar icon set
 | 
					      iconSet: 'mdi-v6', // Quasar icon set
 | 
				
			||||||
      lang: 'de', // Quasar language pack
 | 
					      lang: 'de', // Quasar language pack
 | 
				
			||||||
      config: {
 | 
					      config: {
 | 
				
			||||||
        dark: 'auto',
 | 
					        dark: 'auto',
 | 
				
			||||||
        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
 | 
				
			||||||
| 
						 | 
					@ -109,13 +137,7 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
      // directives: [],
 | 
					      // directives: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Quasar plugins
 | 
					      // Quasar plugins
 | 
				
			||||||
      plugins: [
 | 
					      plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
 | 
				
			||||||
        'LocalStorage',
 | 
					 | 
				
			||||||
        'SessionStorage',
 | 
					 | 
				
			||||||
        'Loading',
 | 
					 | 
				
			||||||
        'Notify',
 | 
					 | 
				
			||||||
        'LoadingBar'
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // animations: 'all', // --- includes all animations
 | 
					    // animations: 'all', // --- includes all animations
 | 
				
			||||||
| 
						 | 
					@ -124,7 +146,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
 | 
				
			||||||
| 
						 | 
					@ -143,20 +165,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
 | 
				
			||||||
| 
						 | 
					@ -166,7 +188,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
 | 
				
			||||||
| 
						 | 
					@ -175,13 +197,11 @@ 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: { ... }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
| 
						 | 
					@ -189,16 +209,19 @@ 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
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  }
 | 
					    bin: {
 | 
				
			||||||
 | 
					      linuxAndroidStudio: '/home/crimsen/.local/share/JetBrains/Toolbox/scripts/studio',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "@quasar/qcalendar": {}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,10 @@
 | 
				
			||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
					// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
				
			||||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
					//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
				
			||||||
import 'quasar/dist/types/feature-flag';
 | 
					import 'quasar/dist/types/feature-flag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module 'quasar/dist/types/feature-flag' {
 | 
					declare module 'quasar/dist/types/feature-flag' {
 | 
				
			||||||
  interface QuasarFeatureFlags {
 | 
					  interface QuasarFeatureFlags {
 | 
				
			||||||
    store: true;
 | 
					    capacitor: true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "appId": "dev.flaschengeist",
 | 
				
			||||||
 | 
					  "appName": "flaschengeist-frontend",
 | 
				
			||||||
 | 
					  "bundledWebRuntime": false,
 | 
				
			||||||
 | 
					  "npmClient": "yarn",
 | 
				
			||||||
 | 
					  "webDir": "www",
 | 
				
			||||||
 | 
					  "android": {
 | 
				
			||||||
 | 
					    "minWebViewVersion": 71
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "ios": {
 | 
				
			||||||
 | 
					    "allowsLinkPreview": false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					  <head>
 | 
				
			||||||
 | 
					    <title>Quasar</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta name="description" content="Quasar Capacitor App">
 | 
				
			||||||
 | 
					    <meta name="format-detection" content="telephone=no">
 | 
				
			||||||
 | 
					    <meta name="msapplication-tap-highlight" content="no">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, viewport-fit=cover">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					      .page {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        height: 100vh;
 | 
				
			||||||
 | 
					        text-align: center;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div class="page">
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        This file will be auto-generated. Do not edit.
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        Run "quasar dev" or "quasar build" with Capacitor mode.
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "flaschengeist",
 | 
				
			||||||
 | 
					  "version": "2.0.0",
 | 
				
			||||||
 | 
					  "description": "Modular student club administration system",
 | 
				
			||||||
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@capacitor/android": "^5.0.0-beta.0",
 | 
				
			||||||
 | 
					    "@capacitor/app": "^5.0.0",
 | 
				
			||||||
 | 
					    "@capacitor/cli": "^5.0.0",
 | 
				
			||||||
 | 
					    "@capacitor/core": "^5.0.0",
 | 
				
			||||||
 | 
					    "@capacitor/ios": "^5.0.0",
 | 
				
			||||||
 | 
					    "@capacitor/preferences": "^5.0.0",
 | 
				
			||||||
 | 
					    "@capacitor/splash-screen": "^5.0.0"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
.DS_Store
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Generated by package manager
 | 
					 | 
				
			||||||
node_modules/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Generated by Cordova
 | 
					 | 
				
			||||||
/plugins/
 | 
					 | 
				
			||||||
/platforms/
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,76 +0,0 @@
 | 
				
			||||||
<?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</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:*" />
 | 
					 | 
				
			||||||
        <icon density="ldpi" src="res/android/ldpi.png" />
 | 
					 | 
				
			||||||
        <icon density="mdpi" src="res/android/mdpi.png" />
 | 
					 | 
				
			||||||
        <icon density="hdpi" src="res/android/hdpi.png" />
 | 
					 | 
				
			||||||
        <icon density="xhdpi" src="res/android/xhdpi.png" />
 | 
					 | 
				
			||||||
        <icon density="xxhdpi" src="res/android/xxhdpi.png" />
 | 
					 | 
				
			||||||
        <icon density="xxxhdpi" src="res/android/xxxhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-ldpi" src="res/screen/android/splash-land-ldpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-ldpi" src="res/screen/android/splash-port-ldpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-mdpi" src="res/screen/android/splash-land-mdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-mdpi" src="res/screen/android/splash-port-mdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-hdpi" src="res/screen/android/splash-land-hdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-hdpi" src="res/screen/android/splash-port-hdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-xhdpi" src="res/screen/android/splash-land-xhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-xhdpi" src="res/screen/android/splash-port-xhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-xxhdpi" src="res/screen/android/splash-land-xxhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-xxhdpi" src="res/screen/android/splash-port-xxhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="land-xxxhdpi" src="res/screen/android/splash-land-xxxhdpi.png" />
 | 
					 | 
				
			||||||
        <splash density="port-xxxhdpi" src="res/screen/android/splash-port-xxxhdpi.png" />
 | 
					 | 
				
			||||||
    </platform>
 | 
					 | 
				
			||||||
    <platform name="ios">
 | 
					 | 
				
			||||||
        <allow-intent href="itms:*" />
 | 
					 | 
				
			||||||
        <allow-intent href="itms-apps:*" />
 | 
					 | 
				
			||||||
        <icon height="57" src="res/ios/icon.png" width="57" />
 | 
					 | 
				
			||||||
        <icon height="114" src="res/ios/icon@2x.png" width="114" />
 | 
					 | 
				
			||||||
        <icon height="40" src="res/ios/icon-20@2x.png" width="40" />
 | 
					 | 
				
			||||||
        <icon height="60" src="res/ios/icon-20@3x.png" width="60" />
 | 
					 | 
				
			||||||
        <icon height="29" src="res/ios/icon-29.png" width="29" />
 | 
					 | 
				
			||||||
        <icon height="58" src="res/ios/icon-29@2x.png" width="58" />
 | 
					 | 
				
			||||||
        <icon height="87" src="res/ios/icon-29@3x.png" width="87" />
 | 
					 | 
				
			||||||
        <icon height="80" src="res/ios/icon-40@2x.png" width="80" />
 | 
					 | 
				
			||||||
        <icon height="120" src="res/ios/icon-60@2x.png" width="120" />
 | 
					 | 
				
			||||||
        <icon height="180" src="res/ios/icon-60@3x.png" width="180" />
 | 
					 | 
				
			||||||
        <icon height="20" src="res/ios/icon-20.png" width="20" />
 | 
					 | 
				
			||||||
        <icon height="40" src="res/ios/icon-40.png" width="40" />
 | 
					 | 
				
			||||||
        <icon height="50" src="res/ios/icon-50.png" width="50" />
 | 
					 | 
				
			||||||
        <icon height="100" src="res/ios/icon-50@2x.png" width="100" />
 | 
					 | 
				
			||||||
        <icon height="72" src="res/ios/icon-72.png" width="72" />
 | 
					 | 
				
			||||||
        <icon height="144" src="res/ios/icon-72@2x.png" width="144" />
 | 
					 | 
				
			||||||
        <icon height="76" src="res/ios/icon-76.png" width="76" />
 | 
					 | 
				
			||||||
        <icon height="152" src="res/ios/icon-76@2x.png" width="152" />
 | 
					 | 
				
			||||||
        <icon height="167" src="res/ios/icon-83.5@2x.png" width="167" />
 | 
					 | 
				
			||||||
        <icon height="1024" src="res/ios/icon-1024.png" width="1024" />
 | 
					 | 
				
			||||||
        <icon height="48" src="res/ios/icon-24@2x.png" width="48" />
 | 
					 | 
				
			||||||
        <icon height="55" src="res/ios/icon-27.5@2x.png" width="55" />
 | 
					 | 
				
			||||||
        <icon height="88" src="res/ios/icon-44@2x.png" width="88" />
 | 
					 | 
				
			||||||
        <icon height="172" src="res/ios/icon-86@2x.png" width="172" />
 | 
					 | 
				
			||||||
        <icon height="196" src="res/ios/icon-98@2x.png" width="196" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@2x~iphone~anyany.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@2x~iphone~comany.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@2x~iphone~comcom.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@3x~iphone~anyany.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@3x~iphone~anycom.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@3x~iphone~comany.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@2x~ipad~anyany.png" />
 | 
					 | 
				
			||||||
        <splash src="res/screen/ios/Default@2x~ipad~comany.png" />
 | 
					 | 
				
			||||||
    </platform>
 | 
					 | 
				
			||||||
    <allow-navigation href="about:*" />
 | 
					 | 
				
			||||||
    <preference name="SplashMaintainAspectRatio" value="true" />
 | 
					 | 
				
			||||||
</widget>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,10 +0,0 @@
 | 
				
			||||||
/* eslint-disable */
 | 
					 | 
				
			||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
					 | 
				
			||||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
					 | 
				
			||||||
import "quasar/dist/types/feature-flag";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare module "quasar/dist/types/feature-flag" {
 | 
					 | 
				
			||||||
  interface QuasarFeatureFlags {
 | 
					 | 
				
			||||||
    cordova: true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,31 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "name": "de.wu5.flaschengeist",
 | 
					 | 
				
			||||||
  "displayName": "Flaschengeist",
 | 
					 | 
				
			||||||
  "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.0.0",
 | 
					 | 
				
			||||||
    "cordova-ios": "^6.1.1",
 | 
					 | 
				
			||||||
    "cordova-plugin-splashscreen": "^6.0.0",
 | 
					 | 
				
			||||||
    "cordova-plugin-whitelist": "^1.3.4"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "cordova": {
 | 
					 | 
				
			||||||
    "plugins": {
 | 
					 | 
				
			||||||
      "cordova-plugin-whitelist": {},
 | 
					 | 
				
			||||||
      "cordova-plugin-splashscreen": {}
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "platforms": [
 | 
					 | 
				
			||||||
      "ios",
 | 
					 | 
				
			||||||
      "android"
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
| 
		 Before Width: | Height: | Size: 913 B  | 
| 
		 Before Width: | Height: | Size: 1.4 KiB  | 
| 
		 Before Width: | Height: | Size: 2.9 KiB  | 
| 
		 Before Width: | Height: | Size: 4.3 KiB  | 
| 
		 Before Width: | Height: | Size: 5.4 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
| 
		 Before Width: | Height: | Size: 370 B  | 
| 
		 Before Width: | Height: | Size: 702 B  | 
| 
		 Before Width: | Height: | Size: 1022 B  | 
| 
		 Before Width: | Height: | Size: 801 B  | 
| 
		 Before Width: | Height: | Size: 969 B  | 
| 
		 Before Width: | Height: | Size: 529 B  | 
| 
		 Before Width: | Height: | Size: 1015 B  | 
| 
		 Before Width: | Height: | Size: 1.5 KiB  | 
| 
		 Before Width: | Height: | Size: 702 B  | 
| 
		 Before Width: | Height: | Size: 1.3 KiB  | 
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 885 B  | 
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 1.1 KiB  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
| 
		 Before Width: | Height: | Size: 1.3 KiB  | 
| 
		 Before Width: | Height: | Size: 2.4 KiB  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB  | 
| 
		 Before Width: | Height: | Size: 3.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.2 KiB  | 
| 
		 Before Width: | Height: | Size: 996 B  | 
| 
		 Before Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 5.3 KiB  | 
| 
		 Before Width: | Height: | Size: 4.1 KiB  | 
| 
		 Before Width: | Height: | Size: 5.6 KiB  | 
| 
		 Before Width: | Height: | Size: 5.9 KiB  | 
| 
		 Before Width: | Height: | Size: 6.5 KiB  | 
| 
		 Before Width: | Height: | Size: 10 KiB  | 
| 
		 Before Width: | Height: | Size: 6.0 KiB  | 
| 
		 Before Width: | Height: | Size: 4.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.0 KiB  | 
| 
		 Before Width: | Height: | Size: 5.1 KiB  | 
| 
		 Before Width: | Height: | Size: 6.1 KiB  | 
| 
		 Before Width: | Height: | Size: 10 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 9.9 KiB  | 
| 
		 Before Width: | Height: | Size: 9.3 KiB  | 
| 
		 Before Width: | Height: | Size: 5.4 KiB  | 
| 
		 Before Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 9.7 KiB  | 
| 
		 Before Width: | Height: | Size: 8.5 KiB  | 
| 
						 | 
					@ -1,9 +1,9 @@
 | 
				
			||||||
/* eslint-disable */
 | 
					/* eslint-disable */
 | 
				
			||||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
					// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
				
			||||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
					//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
				
			||||||
import "quasar/dist/types/feature-flag";
 | 
					import 'quasar/dist/types/feature-flag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module "quasar/dist/types/feature-flag" {
 | 
					declare module 'quasar/dist/types/feature-flag' {
 | 
				
			||||||
  interface QuasarFeatureFlags {
 | 
					  interface QuasarFeatureFlags {
 | 
				
			||||||
    electron: true;
 | 
					    electron: true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +1,77 @@
 | 
				
			||||||
import config from 'src/config';
 | 
					/**
 | 
				
			||||||
 | 
					 * This boot file registers interceptors for axios
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { useMainStore, api } from '@flaschengeist/api';
 | 
				
			||||||
 | 
					import { AxiosError } from 'axios';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
import { LocalStorage, Notify } from 'quasar';
 | 
					import config from 'src/config';
 | 
				
			||||||
import axios, { AxiosError } from 'axios';
 | 
					import { clone } from '@flaschengeist/api';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const api = axios.create();
 | 
					/**
 | 
				
			||||||
 | 
					 * Minify data sent to backend server
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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') {
 | 
				
			||||||
 | 
					    const obj = entity as { [index: string]: unknown };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const prop in obj) {
 | 
				
			||||||
 | 
					      if (obj.hasOwnProperty(prop) && !!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));
 | 
				
			||||||
 | 
					        } else if (
 | 
				
			||||||
 | 
					          typeof obj[prop] === 'object' &&
 | 
				
			||||||
 | 
					          Object.keys(<object>obj[prop]).includes('id') &&
 | 
				
			||||||
 | 
					          typeof (<{ id: unknown }>obj[prop])['id'] === 'number' &&
 | 
				
			||||||
 | 
					          !isNaN((<{ id: number }>obj[prop])['id'])
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          obj[prop] = (<{ id: unknown }>obj[prop])['id'];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return obj;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return entity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ router }) => {
 | 
					export default boot(({ router }) => {
 | 
				
			||||||
  api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
 | 
					  // Persisted value is read in plugins.ts boot file!
 | 
				
			||||||
 | 
					  if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /***
 | 
					  /***
 | 
				
			||||||
   * Intercept requests and insert Token if available
 | 
					   * Intercept requests
 | 
				
			||||||
 | 
					   *   - insert Token if available
 | 
				
			||||||
 | 
					   *   - minify JSON requests
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  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 = { Authorization: 'Bearer ' + store.session.token };
 | 
					      config.headers = Object.assign(config.headers || {}, {
 | 
				
			||||||
 | 
					        Authorization: `Bearer ${store.session.token}`,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Minify JSON requests
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !!config.data &&
 | 
				
			||||||
 | 
					      (config.headers === undefined ||
 | 
				
			||||||
 | 
					        config.headers['Content-Type'] === undefined ||
 | 
				
			||||||
 | 
					        config.headers['Content-Type'] === 'application/json')
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					      config.data = minify(config.data);
 | 
				
			||||||
    return config;
 | 
					    return config;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /***
 | 
					  /***
 | 
				
			||||||
   * Intercept responses
 | 
					   * Intercept responses
 | 
				
			||||||
   *   - filter 401 --> logout
 | 
					   *   - filter 401 --> handleLoggedOut
 | 
				
			||||||
   *   - filter timeout or 502-504 --> backendOffline
 | 
					   *   - filter timeout or 502-504 --> backendOffline
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  api.interceptors.response.use(
 | 
					  api.interceptors.response.use(
 | 
				
			||||||
| 
						 | 
					@ -45,12 +94,11 @@ 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) {
 | 
				
			||||||
          void store.logout();
 | 
					          store.handleLoggedOut();
 | 
				
			||||||
          if (current.name !== 'login') {
 | 
					          if (current.name != 'login') {
 | 
				
			||||||
            await router.push({
 | 
					            await router.push({
 | 
				
			||||||
              name: 'login',
 | 
					              name: 'login',
 | 
				
			||||||
              params: { logout: 'logout' },
 | 
					              query: { redirect: current.fullPath },
 | 
				
			||||||
              query: { redirect: current.path },
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -59,19 +107,3 @@ 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);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,97 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					// eslint-disable-next-line
 | 
				
			||||||
 | 
					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' });
 | 
				
			||||||
 | 
					        if (Platform.is.capacitor) {
 | 
				
			||||||
 | 
					          //void router.push({ name: 'setup_backend' })
 | 
				
			||||||
 | 
					          Notify.create({
 | 
				
			||||||
 | 
					            type: 'negative',
 | 
				
			||||||
 | 
					            message:
 | 
				
			||||||
 | 
					              'Backend nicht erreichbar! Prüfe deine Internetverbindung oder probiere es später nochmal.',
 | 
				
			||||||
 | 
					            timeout: 0,
 | 
				
			||||||
 | 
					            icon: 'mdi-alert-circle-outline',
 | 
				
			||||||
 | 
					            closeBtn: true,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,14 +0,0 @@
 | 
				
			||||||
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,43 +1,33 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This boot file registers login / authentification related axios interceptors
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { useMainStore, hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					 | 
				
			||||||
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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (to.path == from.path) return next();
 | 
					    // Skip loops
 | 
				
			||||||
 | 
					    if (to.name == 'login' && from.name == 'login') return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (to.path.startsWith('/main')) {
 | 
					    // Secured area '/in/...' requires to be authenticated
 | 
				
			||||||
      // Secured area (LOGIN REQUIRED)
 | 
					    if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
 | 
				
			||||||
      // Check login is ok
 | 
					      store.handleLoggedOut();
 | 
				
			||||||
      if (!store.session || store.session.expires <= new Date()) {
 | 
					      return { name: 'login' };
 | 
				
			||||||
        void store.logout();
 | 
					 | 
				
			||||||
        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,60 +1,25 @@
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import routes from 'src/router/routes';
 | 
					 | 
				
			||||||
import { api } from 'boot/axios';
 | 
					 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					 | 
				
			||||||
import { RouteRecordRaw } from 'vue-router';
 | 
					import { RouteRecordRaw } from 'vue-router';
 | 
				
			||||||
import { Notify } from 'quasar';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const config: { [key: string]: Array<string> } = {
 | 
					 | 
				
			||||||
  // Do not change required Modules !!
 | 
					 | 
				
			||||||
  requiredModules: ['User'],
 | 
					 | 
				
			||||||
  // here you can import plugins.
 | 
					 | 
				
			||||||
  loadModules: ['Balance', 'Schedule', 'Pricelist'],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Stop!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// do not change anything here !!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// You can not even read? I said stop!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Really are you stupid? Stop scrolling down here!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Every line you scroll down, an unicorn will die painfully!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Ok you must hate unicorns... But what if I say you I joked... Baby otters will die!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
         .-"""-.
 | 
					 | 
				
			||||||
        /      o\
 | 
					 | 
				
			||||||
       |    o   0).-.
 | 
					 | 
				
			||||||
       |       .-;(_/     .-.
 | 
					 | 
				
			||||||
        \     /  /)).---._|  `\   ,
 | 
					 | 
				
			||||||
         '.  '  /((       `'-./ _/|
 | 
					 | 
				
			||||||
           \  .'  )        .-.;`  /
 | 
					 | 
				
			||||||
            '.             |  `\-'
 | 
					 | 
				
			||||||
              '._        -'    /
 | 
					 | 
				
			||||||
                 ``""--`------`
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/****************************************************
 | 
					/****************************************************
 | 
				
			||||||
 ******** Internal area for some magic **************
 | 
					 ******** Internal area for some magic **************
 | 
				
			||||||
 ****************************************************/
 | 
					 ****************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BackendPlugin {
 | 
					declare type ImportPlgn = { default: FG_Plugin.Plugin };
 | 
				
			||||||
  permissions: string[];
 | 
					
 | 
				
			||||||
  version: string;
 | 
					function validatePlugin(plugin: FG_Plugin.Plugin) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    typeof plugin.name === 'string' &&
 | 
				
			||||||
 | 
					    typeof plugin.id === 'string' &&
 | 
				
			||||||
 | 
					    plugin.id.length > 0 &&
 | 
				
			||||||
 | 
					    typeof plugin.version === 'string'
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BackendPlugins {
 | 
					// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
 | 
				
			||||||
  [key: string]: BackendPlugin;
 | 
					const PLUGINS = <Array<Promise<ImportPlgn>>>[
 | 
				
			||||||
}
 | 
					  /*INSERT_PLUGIN_LIST*/
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
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;
 | 
				
			||||||
| 
						 | 
					@ -221,46 +186,27 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Load a Flaschengeist plugin
 | 
					 * Load a Flaschengeist plugin
 | 
				
			||||||
 * @param loadedPlugins Flaschgeist object
 | 
					 * @param loadedPlugins Flaschgeist object
 | 
				
			||||||
 * @param pluginName Plugin to load
 | 
					 * @param plugin Plugin to load
 | 
				
			||||||
 * @param context RequireContext of plugins
 | 
					 | 
				
			||||||
 * @param router VueRouter instance
 | 
					 * @param router VueRouter instance
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function loadPlugin(
 | 
					function loadPlugin(
 | 
				
			||||||
  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
					  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
				
			||||||
  pluginName: string,
 | 
					  plugin: FG_Plugin.Plugin,
 | 
				
			||||||
  context: __WebpackModuleApi.RequireContext,
 | 
					  backend: FG.Backend
 | 
				
			||||||
  backend: Backend
 | 
					 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  // Check if already loaded
 | 
					  // Check if already loaded
 | 
				
			||||||
  if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true;
 | 
					  if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Search if plugin is installed
 | 
					  // Check backend dependencies
 | 
				
			||||||
  const available = context.keys();
 | 
					 | 
				
			||||||
  const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
 | 
					 | 
				
			||||||
    ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
 | 
					 | 
				
			||||||
      <FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
 | 
					 | 
				
			||||||
    : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!plugin) {
 | 
					 | 
				
			||||||
    // Plugin is not found, results in an error
 | 
					 | 
				
			||||||
    console.exception(`Could not find required Plugin ${pluginName}`);
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    // Plugin found. Check backend dependencies
 | 
					 | 
				
			||||||
  if (
 | 
					  if (
 | 
				
			||||||
      !plugin.requiredBackendModules.every((required) => backend.plugins[required] !== undefined)
 | 
					    !plugin.requiredModules.every(
 | 
				
			||||||
    ) {
 | 
					      (required) =>
 | 
				
			||||||
      console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
 | 
					        backend.plugins[required[0]] !== undefined &&
 | 
				
			||||||
      return false;
 | 
					        (required.length == 1 ||
 | 
				
			||||||
    }
 | 
					          true) /* validate the version, semver440 from python is... tricky on node*/
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Check frontend dependencies
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      !plugin.requiredModules.every((required) =>
 | 
					 | 
				
			||||||
        loadPlugin(loadedPlugins, required, context, backend)
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
      console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
 | 
					    console.error(`Plugin ${plugin.id}: Backend modules not satisfied`);
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -284,94 +230,66 @@ function loadPlugin(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (plugin.widgets.length > 0) {
 | 
					  if (plugin.widgets.length > 0) {
 | 
				
			||||||
      plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
 | 
					    plugin.widgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
 | 
				
			||||||
    Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
 | 
					    Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!plugin.settingWidgets) plugin.settingWidgets = [];
 | 
				
			||||||
 | 
					  if (plugin.settingWidgets.length > 0) {
 | 
				
			||||||
 | 
					    plugin.settingWidgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
 | 
				
			||||||
 | 
					    Array.prototype.push.apply(loadedPlugins.settingWidgets, plugin.settingWidgets);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  loadedPlugins.plugins.push({
 | 
					  loadedPlugins.plugins.push({
 | 
				
			||||||
 | 
					    id: plugin.id,
 | 
				
			||||||
    name: plugin.name,
 | 
					    name: plugin.name,
 | 
				
			||||||
    version: plugin.version,
 | 
					    version: plugin.version,
 | 
				
			||||||
    notification: plugin.notification?.bind({}) || translateNotification,
 | 
					    notification: plugin.notification?.bind({}) || translateNotification,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return plugin;
 | 
					  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 === null) {
 | 
					 | 
				
			||||||
    void router.push({ name: 'error' });
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const loadedPlugins: FG_Plugin.Flaschengeist = {
 | 
					  const loadedPlugins: FG_Plugin.Flaschengeist = {
 | 
				
			||||||
    routes,
 | 
					    routes: baseRoutes,
 | 
				
			||||||
    plugins: [],
 | 
					    plugins: [],
 | 
				
			||||||
    menuLinks: [],
 | 
					    menuLinks: [],
 | 
				
			||||||
    shortcuts: [],
 | 
					    shortcuts: [],
 | 
				
			||||||
    outerShortcuts: [],
 | 
					    outerShortcuts: [],
 | 
				
			||||||
    widgets: [],
 | 
					    widgets: [],
 | 
				
			||||||
 | 
					    settingWidgets: [],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // get all plugins
 | 
					  // Wait for all plugins to be loaded
 | 
				
			||||||
  const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
 | 
					  const results = await Promise.allSettled(PLUGINS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Start loading plugins
 | 
					  // Check if loaded successfully
 | 
				
			||||||
  // Load required modules, if not found or error when loading this will forward the user to the error page
 | 
					  results.forEach((result) => {
 | 
				
			||||||
  config.requiredModules.forEach((required) => {
 | 
					    if (result.status === 'rejected') {
 | 
				
			||||||
    const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
 | 
					      throw <string>result.reason;
 | 
				
			||||||
    if (!plugin) {
 | 
					    } else {
 | 
				
			||||||
      void router.push({ name: 'error' });
 | 
					      if (
 | 
				
			||||||
      return;
 | 
					        !(
 | 
				
			||||||
 | 
					          validatePlugin(result.value.default) &&
 | 
				
			||||||
 | 
					          loadPlugin(loadedPlugins, result.value.default, backend)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					        throw result.value.default.id;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Load user defined plugins
 | 
					 | 
				
			||||||
  // If there is an error with loading a plugin, the user will get informed.
 | 
					 | 
				
			||||||
  const failed: string[] = [];
 | 
					 | 
				
			||||||
  config.loadModules.forEach((required) => {
 | 
					 | 
				
			||||||
    const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
 | 
					 | 
				
			||||||
    if (!plugin) {
 | 
					 | 
				
			||||||
      failed.push(required);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  if (failed.length > 0) {
 | 
					 | 
				
			||||||
    // Log failed plugins
 | 
					 | 
				
			||||||
    console.error('Could not load all plugins', failed);
 | 
					 | 
				
			||||||
    // Inform user about error
 | 
					 | 
				
			||||||
    Notify.create({
 | 
					 | 
				
			||||||
      type: 'negative',
 | 
					 | 
				
			||||||
      message:
 | 
					 | 
				
			||||||
        'Fehler beim Laden: Nicht alle Funktionen stehen zur Verfügung. Bitte wende dich an den Admin!',
 | 
					 | 
				
			||||||
      timeout: 10000,
 | 
					 | 
				
			||||||
      progress: true,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Sort widgets by priority
 | 
					  // Sort widgets by priority
 | 
				
			||||||
  loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
 | 
					  /** @todo Remove priority with first beta */
 | 
				
			||||||
 | 
					  loadedPlugins.widgets.sort(
 | 
				
			||||||
 | 
					    (a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add loaded routes to router
 | 
					  /** @todo Can be cleaned up with first beta */
 | 
				
			||||||
  loadedPlugins.routes.forEach((route) => router.addRoute(route));
 | 
					  loadedPlugins.menuLinks.sort((a, b) => {
 | 
				
			||||||
 | 
					    const diff = a.order && b.order ? b.order - a.order : 0;
 | 
				
			||||||
 | 
					    return diff ? diff : a.title.toString().localeCompare(b.title.toString());
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // save plugins in VM-variable
 | 
					  return loadedPlugins;
 | 
				
			||||||
  app.provide('flaschengeist', loadedPlugins);
 | 
					}
 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||