Compare commits
No commits in common. "main" and "capacitor" have entirely different histories.
26
.eslintrc.js
26
.eslintrc.js
|
@ -17,11 +17,11 @@ module.exports = {
|
||||||
project: resolve(__dirname, './tsconfig.json'),
|
project: resolve(__dirname, './tsconfig.json'),
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
||||||
sourceType: 'module', // Allows for the use of imports
|
sourceType: 'module' // Allows for the use of imports
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rules order is important, please avoid shuffling them
|
// Rules order is important, please avoid shuffling them
|
||||||
|
@ -44,7 +44,7 @@ module.exports = {
|
||||||
|
|
||||||
// https://github.com/prettier/eslint-config-prettier#installation
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
'plugin:prettier/recommended',
|
'prettier', //'plugin:prettier/recommended'
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -54,6 +54,10 @@ module.exports = {
|
||||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
'vue',
|
'vue',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||||
|
// Prettier has not been included as plugin to avoid performance impact
|
||||||
|
// add it as an extension for your IDE
|
||||||
],
|
],
|
||||||
|
|
||||||
globals: {
|
globals: {
|
||||||
|
@ -66,7 +70,7 @@ module.exports = {
|
||||||
__QUASAR_SSR_PWA__: true,
|
__QUASAR_SSR_PWA__: true,
|
||||||
process: true,
|
process: true,
|
||||||
Capacitor: true,
|
Capacitor: true,
|
||||||
chrome: true,
|
chrome: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// add your custom rules here
|
// add your custom rules here
|
||||||
|
@ -76,16 +80,20 @@ module.exports = {
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
|
|
||||||
// Rejects on promises should always be of the Error type (and allow empty rejects as well)
|
// Rejects on promises should always be of the Error type (and allow empty rejects as well)
|
||||||
'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
|
'prefer-promise-reject-errors': ["error", {"allowEmptyReject": true}],
|
||||||
|
|
||||||
// Allow " if ' is contained inside the string, so we can avoid escaping
|
// Allow " if ' is contained inside the string, so we can avoid escaping
|
||||||
quotes: ['error', 'single', { avoidEscape: true }],
|
quotes: [
|
||||||
|
process.env.NODE_ENV === 'production' ? 'error' : 'warn',
|
||||||
|
'single',
|
||||||
|
{ avoidEscape: true }
|
||||||
|
],
|
||||||
|
|
||||||
// TypeScript, let us be not too strict
|
// TypeScript, let us be not too strict
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
|
||||||
// allow debugger during development only
|
// allow debugger during development only
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
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')
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
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
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
pipeline:
|
|
||||||
lint:
|
|
||||||
when:
|
|
||||||
branch: [main, develop]
|
|
||||||
image: node:lts-alpine
|
|
||||||
commands:
|
|
||||||
- yarn install
|
|
||||||
- yarn lint
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
# Flaschengeist (frontend)
|
# Flaschengeist (frontend)
|
||||||
![status-badge](https://ci.os-sc.org/api/badges/Flaschengeist/flaschengeist-frontend/status.svg)
|
|
||||||
|
|
||||||
|
|
||||||
Modular student club administration system, licensed under the MIT license.
|
Modular student club administration system, licensed under the MIT license.
|
||||||
|
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,5 +1,4 @@
|
||||||
import IsoDateInput from './IsoDateInput.vue';
|
import IsoDateInput from './IsoDateInput.vue';
|
||||||
import PasswordInput from './PasswordInput.vue';
|
import PasswordInput from './PasswordInput.vue';
|
||||||
import UserAvatar from './UserAvatar.vue';
|
|
||||||
|
|
||||||
export { IsoDateInput, PasswordInput, UserAvatar };
|
export { IsoDateInput, PasswordInput };
|
||||||
|
|
|
@ -5,6 +5,5 @@ export * from './src/stores/';
|
||||||
export * from './src/utils/datetime';
|
export * from './src/utils/datetime';
|
||||||
export * from './src/utils/permission';
|
export * from './src/utils/permission';
|
||||||
export * from './src/utils/persistent';
|
export * from './src/utils/persistent';
|
||||||
export * from './src/utils/user';
|
|
||||||
export * from './src/utils/validators';
|
export * from './src/utils/validators';
|
||||||
export * from './src/utils/misc';
|
export * from './src/utils/misc';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0-alpha.4",
|
||||||
"name": "@flaschengeist/api",
|
"name": "@flaschengeist/api",
|
||||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||||
|
@ -8,16 +8,27 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||||
|
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||||
|
},
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@quasar/app-webpack": "^3.7.2",
|
"@quasar/app": "^3.2.3",
|
||||||
"flaschengeist": "^2.0.0",
|
"flaschengeist": "^2.0.0-alpha.1",
|
||||||
"pinia": "^2.0.8"
|
"pinia": "^2.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@flaschengeist/types": "^1.0.0",
|
"@flaschengeist/types": "^1.0.0-alpha.5",
|
||||||
"@types/node": "^14.18.0",
|
"@types/node": "^12.20.37",
|
||||||
"typescript": "^4.5.4"
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
|
"eslint": "^8.3.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-vue": "^8.1.1",
|
||||||
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
|
|
@ -4,18 +4,18 @@ import { AxiosResponse } from 'axios';
|
||||||
import { api } from '../internal';
|
import { api } from '../internal';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { PersistentStorage } from '../utils/persistent';
|
import { PersistentStorage } from '../utils/persistent';
|
||||||
import { LocalStorage, SessionStorage } from 'quasar';
|
|
||||||
function reviveSession() {
|
function loadToken() {
|
||||||
return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
|
return PersistentStorage.get<string>('fg_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPersistant() {
|
function clearToken() {
|
||||||
void PersistentStorage.remove('fg_session');
|
void PersistentStorage.remove('fg_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSession(session?: FG.Session) {
|
export function saveToken(token?: string) {
|
||||||
if (session === undefined) return clearPersistant();
|
if (token === undefined) return clearToken();
|
||||||
PersistentStorage.set('fg_session', session).catch(() =>
|
PersistentStorage.set('fg_token', token).catch(() =>
|
||||||
console.error('Could not save token to storage')
|
console.error('Could not save token to storage')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,6 @@ export const useMainStore = defineStore({
|
||||||
if (this.user === undefined) throw 'Not logged in, this should not be called';
|
if (this.user === undefined) throw 'Not logged in, this should not be called';
|
||||||
return this.user;
|
return this.user;
|
||||||
},
|
},
|
||||||
currentSession(): FG.Session {
|
|
||||||
if (this.session === undefined) throw 'Not logged in, this should not be called';
|
|
||||||
return this.session;
|
|
||||||
},
|
|
||||||
permissions(): string[] {
|
permissions(): string[] {
|
||||||
return this.user?.permissions || [];
|
return this.user?.permissions || [];
|
||||||
},
|
},
|
||||||
|
@ -56,9 +52,9 @@ export const useMainStore = defineStore({
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.session = await reviveSession();
|
const token = await loadToken();
|
||||||
if (this.session !== undefined) {
|
if (token !== null) {
|
||||||
this.session = await sessionStore.getSession(this.session.token);
|
this.session = await sessionStore.getSession(token);
|
||||||
if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
|
if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -67,14 +63,11 @@ export const useMainStore = defineStore({
|
||||||
},
|
},
|
||||||
|
|
||||||
async login(userid: string, password: string) {
|
async login(userid: string, password: string) {
|
||||||
const userStore = useUserStore();
|
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post<FG.Session>('/auth', { userid, password });
|
const { data } = await api.post<FG.Session>('/auth', { userid, password });
|
||||||
this.session = fixSession(data);
|
this.session = fixSession(data);
|
||||||
this.user = await userStore.getUser(data.userid, true);
|
|
||||||
return true;
|
return true;
|
||||||
} catch ({ response }) {
|
} catch ({ response }) {
|
||||||
this.handleLoggedOut();
|
|
||||||
return (<AxiosResponse | undefined>response)?.status || false;
|
return (<AxiosResponse | undefined>response)?.status || false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -153,9 +146,7 @@ export const useMainStore = defineStore({
|
||||||
|
|
||||||
handleLoggedOut() {
|
handleLoggedOut() {
|
||||||
this.$reset();
|
this.$reset();
|
||||||
void clearPersistant();
|
void clearToken();
|
||||||
LocalStorage.clear();
|
|
||||||
SessionStorage.clear();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,195 +1,84 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from '../internal';
|
import { api } from '../internal';
|
||||||
import { isAxiosError, useMainStore } from '.';
|
import { isAxiosError, useMainStore } from '.';
|
||||||
import { DisplayNameMode } from '@flaschengeist/users';
|
|
||||||
|
|
||||||
export function fixUser(u?: FG.User) {
|
export function fixUser(u?: FG.User) {
|
||||||
return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
|
return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if state is outdated / dirty
|
|
||||||
* Value is considered outdated after 15 minutes
|
|
||||||
* @param updated Time of last updated (in milliseconds see Date.now())
|
|
||||||
* @returns True if outdated, false otherwise
|
|
||||||
*/
|
|
||||||
function isDirty(updated: number) {
|
|
||||||
return Date.now() - updated > 15 * 60 * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: 'users',
|
id: 'users',
|
||||||
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
roles: [] as FG.Role[],
|
roles: [] as FG.Role[],
|
||||||
|
users: [] as FG.User[],
|
||||||
permissions: [] as FG.Permission[],
|
permissions: [] as FG.Permission[],
|
||||||
userSettings: {} as FG.UserSettings,
|
_dirty_users: true,
|
||||||
// list of all users, include deleted ones, use `users` getter for list of active ones
|
_dirty_roles: true,
|
||||||
_users: [] as FG.User[],
|
|
||||||
// Internal flags for deciding if lists need to force-loaded
|
|
||||||
_dirty_users: 0,
|
|
||||||
_dirty_roles: 0,
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
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: {
|
actions: {
|
||||||
/** Simply filter all users by ID */
|
|
||||||
findUser(userid: string) {
|
findUser(userid: string) {
|
||||||
return this._users.find((user) => user.userid === userid);
|
return this.users.find((user) => user.userid === userid);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Retrieve user by ID
|
|
||||||
* @param userid ID of user to retrieve
|
|
||||||
* @param force If set to true the user is loaded from backend even when a local copy is available
|
|
||||||
* @returns Retrieved user (Promise) or raise an error
|
|
||||||
* @throws Probably an AxiosError if loading failed
|
|
||||||
*/
|
|
||||||
async getUser(userid: string, force = false) {
|
async getUser(userid: string, force = false) {
|
||||||
const idx = this._users.findIndex((user) => user.userid === userid);
|
const idx = this.users.findIndex((user) => user.userid === userid);
|
||||||
if (force || idx === -1 || isDirty(this._dirty_users)) {
|
if (force || this._dirty_users || idx === -1) {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<FG.User>(`/users/${userid}`);
|
const { data } = await api.get<FG.User>(`/users/${userid}`);
|
||||||
fixUser(data);
|
fixUser(data);
|
||||||
if (idx === -1) this._users.push(data);
|
if (idx === -1) this.users.push(data);
|
||||||
else this._users[idx] = data;
|
else this.users[idx] = data;
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore 404, throw all other
|
// Ignore 404, throw all other
|
||||||
if (!isAxiosError(error, 404)) throw error;
|
if (!isAxiosError(error, 404)) throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this._users[idx];
|
return this.users[idx];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Retrieve list of all users
|
|
||||||
* @param force If set to true a fresh users list is loaded from backend even when a local copy is available
|
|
||||||
* @returns Array of retrieved users (Promise)
|
|
||||||
* @throws Probably an AxiosError if loading failed
|
|
||||||
*/
|
|
||||||
async getUsers(force = false) {
|
async getUsers(force = false) {
|
||||||
if (force || isDirty(this._dirty_users)) {
|
if (force || this._dirty_users) {
|
||||||
const { data } = await api.get<FG.User[]>('/users');
|
const { data } = await api.get<FG.User[]>('/users');
|
||||||
data.forEach(fixUser);
|
data.forEach(fixUser);
|
||||||
this._users = data;
|
this.users = data;
|
||||||
this._dirty_users = Date.now();
|
this._dirty_users = false;
|
||||||
}
|
}
|
||||||
return this._users;
|
return this.users;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Save modifications of user on backend
|
|
||||||
* @param user Modified user to save
|
|
||||||
* @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
|
|
||||||
*/
|
|
||||||
async updateUser(user: FG.User) {
|
async updateUser(user: FG.User) {
|
||||||
await api.put(`/users/${user.userid}`, user);
|
await api.put(`/users/${user.userid}`, user);
|
||||||
// Modifcation accepted by backend
|
|
||||||
// Save modifications back to our users list
|
|
||||||
const idx = this._users.findIndex((u) => u.userid === user.userid);
|
|
||||||
if (idx > -1) this._users[idx] = user;
|
|
||||||
// If user was current user, save modifications back to the main store
|
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
if (user.userid === mainStore.user?.userid) mainStore.user = user;
|
if (user.userid === mainStore.user?.userid) mainStore.user = user;
|
||||||
|
this._dirty_users = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Register a new user
|
|
||||||
* @param user User to register (id not set)
|
|
||||||
* @returns The registered user (id set)
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async createUser(user: FG.User) {
|
async createUser(user: FG.User) {
|
||||||
const { data } = await api.post<FG.User>('/users', user);
|
const { data } = await api.post<FG.User>('/users', user);
|
||||||
this._users.push(<FG.User>fixUser(data));
|
this.users.push(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Delete an user
|
async uploadAvatar(user: FG.User, file: string | File) {
|
||||||
* Throws if failed and resolves void if succeed
|
|
||||||
*
|
|
||||||
* @param user User or ID of user to delete
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async deleteUser(user: FG.User | string) {
|
|
||||||
if (typeof user === 'object') user = user.userid;
|
|
||||||
|
|
||||||
await api.delete(`/users/${user}`);
|
|
||||||
this._users = this._users.filter((u) => u.userid != user);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Upload an avatar for an user
|
|
||||||
* Throws if failed and resolves void if succeed
|
|
||||||
*
|
|
||||||
* @param user User or ID of user
|
|
||||||
* @param file Avatar file to upload
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async uploadAvatar(user: FG.User | string, file: string | File) {
|
|
||||||
if (typeof user === 'object') user = user.userid;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
await api.post(`/users/${user}/avatar`, formData, {
|
await api.post(`/users/${user.userid}/avatar`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Delete avatar of an user
|
async deleteAvatar(user: FG.User) {
|
||||||
* @param user User or ID of user
|
await api.delete(`/users/${user.userid}/avatar`);
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async deleteAvatar(user: FG.User | string) {
|
|
||||||
if (typeof user === 'object') user = user.userid;
|
|
||||||
|
|
||||||
await api.delete(`/users/${user}/avatar`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Retrieve list of all permissions
|
|
||||||
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
|
||||||
* @returns Array of retrieved permissions (Promise)
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async getPermissions(force = false) {
|
async getPermissions(force = false) {
|
||||||
if (force || this.permissions.length === 0) {
|
if (force || this.permissions.length === 0) {
|
||||||
const { data } = await api.get<FG.Permission[]>('/roles/permissions');
|
const { data } = await api.get<FG.Permission[]>('/roles/permissions');
|
||||||
|
@ -198,81 +87,39 @@ export const useUserStore = defineStore({
|
||||||
return this.permissions;
|
return this.permissions;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Retrieve list of all roles
|
|
||||||
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
|
||||||
* @returns Array of retrieved roles (Promise)
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async getRoles(force = false) {
|
async getRoles(force = false) {
|
||||||
if (force || isDirty(this._dirty_roles)) {
|
if (force || this._dirty_roles) {
|
||||||
const { data } = await api.get<FG.Role[]>('/roles');
|
const { data } = await api.get<FG.Role[]>('/roles');
|
||||||
this.roles = data;
|
this.roles = data;
|
||||||
this._dirty_roles = Date.now();
|
this._dirty_roles = false;
|
||||||
}
|
}
|
||||||
return this.roles;
|
return this.roles;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Save modifications of role on the backend
|
|
||||||
* @param role role to save
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async updateRole(role: FG.Role) {
|
async updateRole(role: FG.Role) {
|
||||||
|
try {
|
||||||
await api.put(`/roles/${role.id}`, role);
|
await api.put(`/roles/${role.id}`, role);
|
||||||
|
} catch (error) {
|
||||||
const idx = this.roles.findIndex((r) => r.id === role.id);
|
console.warn(error);
|
||||||
if (idx != -1) this.roles[idx] = role;
|
}
|
||||||
else this._dirty_roles = 0;
|
this._updatePermission(role);
|
||||||
|
},
|
||||||
|
|
||||||
|
_updatePermission(role: FG.Role) {
|
||||||
|
const idx = this.roles.findIndex((r) => r.id === role.id);
|
||||||
|
if (idx != -1) this.roles[idx] = role;
|
||||||
|
this._dirty_roles = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Create a new role
|
|
||||||
* @param role Role to create (ID not set)
|
|
||||||
* @returns Created role (ID set)
|
|
||||||
* @throws Probably an AxiosError if request failed
|
|
||||||
*/
|
|
||||||
async newRole(role: FG.Role) {
|
async newRole(role: FG.Role) {
|
||||||
const { data } = await api.post<FG.Role>('/roles', role);
|
const { data } = await api.post<FG.Role>('/roles', role);
|
||||||
this.roles.push(data);
|
this.roles.push(data);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Delete a role
|
|
||||||
* @param role Role or ID of role to delete
|
|
||||||
* @throws Probably an AxiosError if request failed (409 if role still in use)
|
|
||||||
*/
|
|
||||||
async deleteRole(role: FG.Role | number) {
|
async deleteRole(role: FG.Role | number) {
|
||||||
if (typeof role === 'object') role = role.id;
|
await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`);
|
||||||
await api.delete(`/roles/${role}`);
|
this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id));
|
||||||
this.roles = this.roles.filter((r) => r.id !== role);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
import { LocalStorage, Platform } from 'quasar';
|
import { LocalStorage, Platform } from 'quasar';
|
||||||
import { Preferences } from '@capacitor/preferences';
|
import { Storage } from '@capacitor/storage';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type PersitentTypes = Date | RegExp | number | boolean | string | object;
|
type PersitentTypes = Date | RegExp | number | boolean | string | object;
|
||||||
|
|
||||||
export class PersistentStorage {
|
export class PersistentStorage {
|
||||||
static clear() {
|
static clear() {
|
||||||
if (Platform.is.capacitor) return Preferences.clear();
|
if (Platform.is.capacitor) return Storage.clear();
|
||||||
else return Promise.resolve(LocalStorage.clear());
|
else return Promise.resolve(LocalStorage.clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
static remove(key: string) {
|
static remove(key: string) {
|
||||||
if (Platform.is.capacitor) return Preferences.remove({ key: key });
|
if (Platform.is.capacitor) return Storage.remove({ key: key });
|
||||||
else return Promise.resolve(LocalStorage.remove(key));
|
else return Promise.resolve(LocalStorage.remove(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
static set(key: string, value: PersitentTypes) {
|
static set(key: string, value: PersitentTypes) {
|
||||||
if (Platform.is.capacitor) return Preferences.set({ key, value: JSON.stringify(value) });
|
if (Platform.is.capacitor) return Storage.set({ key, value: JSON.stringify(value) });
|
||||||
else return Promise.resolve(LocalStorage.set(key, value));
|
else return Promise.resolve(LocalStorage.set(key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
static get<T extends PersitentTypes>(key: string) {
|
static get<T extends PersitentTypes>(key: string) {
|
||||||
if (Platform.is.capacitor)
|
if (Platform.is.capacitor)
|
||||||
return Preferences.get({ key }).then((v) =>
|
return Storage.get({ key }).then((v) =>
|
||||||
v.value === null ? null : (JSON.parse(v.value) as T)
|
v.value === null ? null : (JSON.parse(v.value) as T)
|
||||||
);
|
);
|
||||||
else return Promise.resolve(LocalStorage.getItem<T>(key));
|
else return Promise.resolve(LocalStorage.getItem<T>(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
static keys() {
|
static keys() {
|
||||||
if (Platform.is.capacitor) return Preferences.keys().then((v) => v.keys);
|
if (Platform.is.capacitor) return Storage.keys().then((v) => v.keys);
|
||||||
else return Promise.resolve(LocalStorage.getAllKeys());
|
else return Promise.resolve(LocalStorage.getAllKeys());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { api } from '../internal';
|
|
||||||
|
|
||||||
export function avatarURL(user: FG.User | string, thumbnail = true) {
|
|
||||||
if (typeof user === 'object') user = user.userid;
|
|
||||||
return `${api.defaults?.baseURL || ''}/users/${user}/avatar${thumbnail ? '?thumbnail' : ''}`;
|
|
||||||
}
|
|
|
@ -3,7 +3,14 @@
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"lib": ["es2020", "dom"],
|
"lib": [
|
||||||
"types": ["@flaschengeist/types", "@quasar/app", "node"]
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"@flaschengeist/types",
|
||||||
|
"@quasar/app",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['@quasar/babel-preset-app'],
|
presets: [
|
||||||
};
|
'@quasar/babel-preset-app'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
51
package.json
51
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "2.1.0",
|
"version": "2.0.0-alpha.1",
|
||||||
"productName": "flaschengeist-frontend",
|
"productName": "flaschengeist-frontend",
|
||||||
"name": "flaschengeist",
|
"name": "flaschengeist",
|
||||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
|
@ -11,41 +11,33 @@
|
||||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --config ./package.json --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
|
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./src ./api"
|
"lint": "eslint --ext .js,.ts,.vue ./src ./api"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flaschengeist/api": "^1.0.0",
|
"@capacitor/storage": "^1.2.3",
|
||||||
"@flaschengeist/balance": "^1.0.0",
|
"@flaschengeist/api": "file:./api",
|
||||||
"@flaschengeist/pricelist-old": "^1.0.0",
|
"@flaschengeist/users": "^1.0.0-alpha.1",
|
||||||
"@flaschengeist/schedule": "^1.0.0",
|
"axios": "^0.24.0",
|
||||||
"@flaschengeist/users": "^1.0.0",
|
"pinia": "^2.0.4",
|
||||||
"axios": "^1.4.0",
|
"quasar": "^2.3.3"
|
||||||
"pinia": "^2.0.8",
|
|
||||||
"quasar": "^2.11.10",
|
|
||||||
"vue": "^3.0.0",
|
|
||||||
"vue-router": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/core": "^5.0.0",
|
"@flaschengeist/types": "^1.0.0-alpha.6",
|
||||||
"@capacitor/preferences": "^5.0.0",
|
"@quasar/app": "^3.2.3",
|
||||||
"@flaschengeist/types": "^1.0.0",
|
"@quasar/extras": "^1.12.1",
|
||||||
"@quasar/app-webpack": "^3.7.2",
|
"@types/node": "^14.17.34",
|
||||||
"@quasar/extras": "^1.16.3",
|
|
||||||
"@types/node": "^14.18.0",
|
|
||||||
"@types/webpack": "^5.28.0",
|
"@types/webpack": "^5.28.0",
|
||||||
"@types/webpack-env": "^1.16.3",
|
"@types/webpack-env": "^1.16.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||||
"@typescript-eslint/parser": "^5.8.0",
|
"@typescript-eslint/parser": "^5.4.0",
|
||||||
"@vue/devtools": "^6.5.0",
|
"eslint": "^8.3.0",
|
||||||
"eslint": "^8.5.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-vue": "^8.1.1",
|
||||||
"eslint-plugin-vue": "^9.14.1",
|
"eslint-webpack-plugin": "^3.1.1",
|
||||||
"eslint-webpack-plugin": "^4.0.1",
|
"modify-source-webpack-plugin": "^3.0.0",
|
||||||
"modify-source-webpack-plugin": "^4.1.0",
|
"prettier": "^2.4.1",
|
||||||
"prettier": "^2.5.1",
|
"typescript": "^4.5.2",
|
||||||
"typescript": "^4.5.4",
|
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
@ -66,8 +58,7 @@
|
||||||
],
|
],
|
||||||
"cordova": [
|
"cordova": [
|
||||||
"iOS >= 13.0",
|
"iOS >= 13.0",
|
||||||
"Android >= 76",
|
"Android >= 76"
|
||||||
"ChromeAndroid >= 76"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -3,4 +3,4 @@ module.exports = [
|
||||||
// '@flaschengeist/balance',
|
// '@flaschengeist/balance',
|
||||||
// '@flaschengeist/schedule',
|
// '@flaschengeist/schedule',
|
||||||
// '@flaschengeist/pricelist',
|
// '@flaschengeist/pricelist',
|
||||||
'@flaschengeist/schedule',
|
]
|
||||||
|
|
106
quasar.conf.js
106
quasar.conf.js
|
@ -8,23 +8,10 @@
|
||||||
|
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||||
const { ModifySourcePlugin, ReplaceOperation } = require('modify-source-webpack-plugin');
|
const { ModifySourcePlugin } = 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 {
|
||||||
|
@ -35,7 +22,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/prefetch-feature
|
// https://quasar.dev/quasar-cli/prefetch-feature
|
||||||
|
@ -44,7 +31,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://quasar.dev/quasar-cli/boot-files
|
// https://quasar.dev/quasar-cli/boot-files
|
||||||
boot: ['axios', 'store', 'plugins', 'login', 'init'],
|
boot: ['axios', 'store', 'plugins', 'loading', 'login'],
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
||||||
css: ['app.scss'],
|
css: ['app.scss'],
|
||||||
|
@ -56,7 +43,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// 'ionicons-v5',
|
// 'ionicons-v5',
|
||||||
// 'line-awesome',
|
// 'line-awesome',
|
||||||
// 'material-icons',
|
// 'material-icons',
|
||||||
'mdi-v7',
|
'mdi-v6',
|
||||||
// 'themify',
|
// 'themify',
|
||||||
|
|
||||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
@ -66,9 +53,10 @@ 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)
|
||||||
// (from node_modules, which are by default not transpiled).
|
// (from node_modules, which are by default not transpiled).
|
||||||
// Applies only if "transpile" is set to true.
|
// Applies only if "transpile" is set to true.
|
||||||
|
@ -84,28 +72,32 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// https://quasar.dev/quasar-cli/handling-webpack
|
// https://quasar.dev/quasar-cli/handling-webpack
|
||||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||||
chainWebpack (chain) {
|
chainWebpack (chain) {
|
||||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
|
chain.plugin('eslint-webpack-plugin')
|
||||||
{
|
.use(ESLintPlugin, [{
|
||||||
extensions: [ 'ts', 'js', 'vue' ],
|
extensions: [ 'ts', 'js', 'vue' ],
|
||||||
exclude: ['node_modules', 'src-capacitor'],
|
exclude: ['node_modules', 'src-capacitor']
|
||||||
},
|
}])
|
||||||
]);
|
chain.plugin('modify-source-webpack-plugin')
|
||||||
chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
|
.use(ModifySourcePlugin, [{
|
||||||
{
|
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /plugins\.ts$/,
|
test: /plugins\.ts$/,
|
||||||
operations: [operation()],
|
modify: (src, filename) => {
|
||||||
},
|
const custom_plgns = require('./plugin.config.js')
|
||||||
],
|
const required_plgns = require('./src/vendor-plugin.config.js')
|
||||||
},
|
return src.replace(/\/\* *INSERT_PLUGIN_LIST *\*\//,
|
||||||
]);
|
[...custom_plgns, ...required_plgns].map(v => `import("${v}").catch(() => "${v}")`)
|
||||||
|
.join(','))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}])
|
||||||
chain.merge({
|
chain.merge({
|
||||||
snapshot: {
|
snapshot: {
|
||||||
managedPaths: [],
|
managedPaths: []
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
||||||
|
@ -113,7 +105,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
https: false,
|
https: false,
|
||||||
port: 8080,
|
port: 8080,
|
||||||
open: false, // opens browser window automatically
|
open: false, // opens browser window automatically
|
||||||
watchFiles: { paths: ['/node_modules/@flaschengeist/**/*'] },
|
watchFiles: {paths: ['/node_modules/@flaschengeist/**/*']}
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
||||||
|
@ -125,8 +117,8 @@ module.exports = configure(function (/* ctx */) {
|
||||||
loadingBar: {
|
loadingBar: {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
size: '5px',
|
size: '5px'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// For special cases outside of where the auto-import stategy can have an impact
|
// For special cases outside of where the auto-import stategy can have an impact
|
||||||
|
@ -137,7 +129,14 @@ module.exports = configure(function (/* ctx */) {
|
||||||
// directives: [],
|
// directives: [],
|
||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
|
plugins: [
|
||||||
|
'LocalStorage',
|
||||||
|
'SessionStorage',
|
||||||
|
'Dialog',
|
||||||
|
'Loading',
|
||||||
|
'Notify',
|
||||||
|
'LoadingBar'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
|
@ -146,7 +145,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||||
ssr: {
|
ssr: {
|
||||||
pwa: false,
|
pwa: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
||||||
|
@ -165,20 +164,20 @@ module.exports = configure(function (/* ctx */) {
|
||||||
{
|
{
|
||||||
src: 'flaschengeist-logo.svg',
|
src: 'flaschengeist-logo.svg',
|
||||||
sizes: 'any',
|
sizes: 'any',
|
||||||
type: 'image/svg+xml',
|
type: 'image/svg+xml'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'favicon-128x128.png',
|
src: 'favicon-128x128.png',
|
||||||
sizes: '128x128',
|
sizes: '128x128',
|
||||||
type: 'image/png',
|
type: 'image/png'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'favicon-256x256.png',
|
src: 'favicon-256x256.png',
|
||||||
sizes: '256x256',
|
sizes: '256x256',
|
||||||
type: 'image/png',
|
type: 'image/png'
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
||||||
|
@ -188,7 +187,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
||||||
capacitor: {
|
capacitor: {
|
||||||
hideSplashscreen: true,
|
hideSplashscreen: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
||||||
|
@ -197,11 +196,13 @@ module.exports = configure(function (/* ctx */) {
|
||||||
|
|
||||||
packager: {
|
packager: {
|
||||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||||
|
|
||||||
// OS X / Mac App Store
|
// OS X / Mac App Store
|
||||||
// appBundleId: '',
|
// appBundleId: '',
|
||||||
// appCategoryType: '',
|
// appCategoryType: '',
|
||||||
// osxSign: '',
|
// osxSign: '',
|
||||||
// protocol: 'myapp://path',
|
// protocol: 'myapp://path',
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
// win32metadata: { ... }
|
// win32metadata: { ... }
|
||||||
},
|
},
|
||||||
|
@ -209,7 +210,7 @@ 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
|
||||||
|
@ -218,10 +219,7 @@ module.exports = configure(function (/* ctx */) {
|
||||||
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,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 {
|
||||||
capacitor: true;
|
capacitor: true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,6 @@
|
||||||
"bundledWebRuntime": false,
|
"bundledWebRuntime": false,
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"webDir": "www",
|
"webDir": "www",
|
||||||
"android": {
|
|
||||||
"minWebViewVersion": 71
|
|
||||||
},
|
|
||||||
"ios": {
|
"ios": {
|
||||||
"allowsLinkPreview": false
|
"allowsLinkPreview": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
<!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>
|
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "flaschengeist",
|
"name": "flaschengeist",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0-alpha.1",
|
||||||
"description": "Modular student club administration system",
|
"description": "Modular student club administration system",
|
||||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^5.0.0-beta.0",
|
"@capacitor/android": "^3.3.2",
|
||||||
"@capacitor/app": "^5.0.0",
|
"@capacitor/app": "^1.0.0",
|
||||||
"@capacitor/cli": "^5.0.0",
|
"@capacitor/cli": "^3.0.0",
|
||||||
"@capacitor/core": "^5.0.0",
|
"@capacitor/core": "^3.0.0",
|
||||||
"@capacitor/ios": "^5.0.0",
|
"@capacitor/ios": "^3.0.0-beta.0",
|
||||||
"@capacitor/preferences": "^5.0.0",
|
"@capacitor/splash-screen": "^1.0.0",
|
||||||
"@capacitor/splash-screen": "^5.0.0"
|
"@capacitor/storage": "^1.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,31 +1,18 @@
|
||||||
/**
|
|
||||||
* This boot file registers interceptors for axios
|
|
||||||
*/
|
|
||||||
import { useMainStore, api } from '@flaschengeist/api';
|
import { useMainStore, api } from '@flaschengeist/api';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import config from 'src/config';
|
import config from 'src/config';
|
||||||
import { clone } from '@flaschengeist/api';
|
import { clone } from '@flaschengeist/api';
|
||||||
|
|
||||||
/**
|
function minify(o: unknown, cloned = false) {
|
||||||
* Minify data sent to backend server
|
if (!cloned) o = clone(o);
|
||||||
*
|
|
||||||
* Drop unneeded entities which can be identified by ID.
|
|
||||||
*
|
|
||||||
* @param obj Object to minify
|
|
||||||
* @param cloned If this entity is already cloned (JSON En+Decoded)
|
|
||||||
* @returns Minified object (some types are converted, like a Date object is now a ISO string)
|
|
||||||
*/
|
|
||||||
function minify(entity: unknown, cloned = false) {
|
|
||||||
if (!cloned) entity = clone(entity);
|
|
||||||
|
|
||||||
if (typeof entity === 'object') {
|
if (typeof o === 'object') {
|
||||||
const obj = entity as { [index: string]: unknown };
|
const obj = o as { [index: string]: unknown };
|
||||||
|
|
||||||
for (const prop in obj) {
|
for (const prop in obj) {
|
||||||
if (obj.hasOwnProperty(prop) && !!obj[prop]) {
|
if (obj.hasOwnProperty(prop) && !!obj[prop]) {
|
||||||
if (Array.isArray(obj[prop])) {
|
if (Array.isArray(obj[prop])) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
||||||
obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
|
obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
|
||||||
} else if (
|
} else if (
|
||||||
typeof obj[prop] === 'object' &&
|
typeof obj[prop] === 'object' &&
|
||||||
|
@ -39,12 +26,12 @@ function minify(entity: unknown, cloned = false) {
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
return entity;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
// Persisted value is read in plugins.ts boot file!
|
// Persisted value is read in plugins.ts boot file!
|
||||||
if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
|
api.defaults.baseURL = config.baseURL;
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Intercept requests
|
* Intercept requests
|
||||||
|
@ -54,9 +41,7 @@ export default boot(({ router }) => {
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
if (store.session?.token) {
|
if (store.session?.token) {
|
||||||
config.headers = Object.assign(config.headers || {}, {
|
config.headers = { Authorization: 'Bearer ' + store.session.token };
|
||||||
Authorization: `Bearer ${store.session.token}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Minify JSON requests
|
// Minify JSON requests
|
||||||
if (
|
if (
|
||||||
|
@ -94,11 +79,12 @@ export default boot(({ router }) => {
|
||||||
query: { redirect: next },
|
query: { redirect: next },
|
||||||
});
|
});
|
||||||
} else if (e.response && e.response.status == 401) {
|
} else if (e.response && e.response.status == 401) {
|
||||||
store.handleLoggedOut();
|
void store.handleLoggedOut();
|
||||||
if (current.name != 'login') {
|
if (current.name !== 'login') {
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
query: { redirect: current.fullPath },
|
params: { logout: 'logout' },
|
||||||
|
query: { redirect: current.path },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,3 +93,5 @@ export default boot(({ router }) => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export { api };
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
/**
|
|
||||||
* This boot file initalizes the store from persistent storage and load all plugins
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
PersistentStorage,
|
|
||||||
api,
|
|
||||||
isAxiosError,
|
|
||||||
saveSession,
|
|
||||||
useMainStore,
|
|
||||||
} from '@flaschengeist/api';
|
|
||||||
import { Notify, Platform } from 'quasar';
|
|
||||||
import { loadPlugins } from './plugins';
|
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import routes from 'src/router/routes';
|
|
||||||
|
|
||||||
async function loadBaseUrl() {
|
|
||||||
try {
|
|
||||||
const url = await PersistentStorage.get<string>('baseURL');
|
|
||||||
if (url !== null) api.defaults.baseURL = url;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Could not load BaseURL', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { Loading } from 'quasar';
|
||||||
|
//import DarkCircularProgress from 'components/loading/DarkCircularProgress.vue';
|
||||||
|
|
||||||
|
// "async" is optional;
|
||||||
|
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
|
||||||
|
export default boot(() => {
|
||||||
|
Loading.setDefaults({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
// spinner: DarkCircularProgress,
|
||||||
|
// TODO : Das funktioniert wohl erstmal nicht mehr... gibt ne exception
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,33 +1,42 @@
|
||||||
/**
|
|
||||||
* This boot file registers login / authentification related axios interceptors
|
|
||||||
*/
|
|
||||||
import { useMainStore, hasPermissions } from '@flaschengeist/api';
|
import { useMainStore, hasPermissions } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { RouteRecord } from 'vue-router';
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
/**
|
router.beforeResolve((to, from, next) => {
|
||||||
* Login guard
|
|
||||||
* Check if user tries to access the secured area and validates token
|
|
||||||
*/
|
|
||||||
router.beforeEach((to, from) => {
|
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
|
|
||||||
// Skip loops
|
if (to.path == from.path) return next();
|
||||||
if (to.name == 'login' && from.name == 'login') return false;
|
|
||||||
|
|
||||||
// Secured area '/in/...' requires to be authenticated
|
if (to.path.startsWith('/main')) {
|
||||||
if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
|
// Secured area (LOGIN REQUIRED)
|
||||||
store.handleLoggedOut();
|
// Check login is ok
|
||||||
return { name: 'login' };
|
if (!store.session || store.session.expires <= new Date()) {
|
||||||
|
void store.handleLoggedOut();
|
||||||
|
return next({ name: 'login', query: { redirect: to.fullPath } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if special permissions are required
|
||||||
|
if (
|
||||||
|
to.matched.every((record: RouteRecord) => {
|
||||||
|
if (!('meta' in record) || !('permissions' in record.meta)) return true;
|
||||||
|
if ((<{ permissions: FG.Permission[] }>record.meta).permissions) {
|
||||||
|
return hasPermissions((<{ permissions: FG.Permission[] }>record.meta).permissions);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
} else {
|
||||||
|
return next({ name: 'login', query: { redirect: to.fullPath } });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (to.name == 'login' && store.user && !to.params['logout']) {
|
||||||
|
// Called login while already logged in
|
||||||
|
return next({ name: 'dashboard' });
|
||||||
|
} else {
|
||||||
|
// We are on the non secured area
|
||||||
|
return next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Permission guard
|
|
||||||
* Check permissions for route, cancel navigation on errors
|
|
||||||
*/
|
|
||||||
router.beforeResolve((to) => {
|
|
||||||
if (!!to.meta.permissions && !hasPermissions(<FG.Permission[]>to.meta.permissions))
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { FG_Plugin } from '@flaschengeist/types';
|
import { Notify, Platform } from 'quasar';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import routes from 'src/router/routes';
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
import { PersistentStorage } from '@flaschengeist/api';
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
******** Internal area for some magic **************
|
******** Internal area for some magic **************
|
||||||
|
@ -21,6 +26,21 @@ const PLUGINS = <Array<Promise<ImportPlgn>>>[
|
||||||
/*INSERT_PLUGIN_LIST*/
|
/*INSERT_PLUGIN_LIST*/
|
||||||
];
|
];
|
||||||
|
|
||||||
|
interface BackendPlugin {
|
||||||
|
permissions: string[];
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackendPlugins {
|
||||||
|
[key: string]: BackendPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Backend {
|
||||||
|
plugins: BackendPlugins;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
export { Backend };
|
||||||
|
|
||||||
// Handle Notifications
|
// Handle Notifications
|
||||||
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
||||||
|
|
||||||
|
@ -192,7 +212,7 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
|
||||||
function loadPlugin(
|
function loadPlugin(
|
||||||
loadedPlugins: FG_Plugin.Flaschengeist,
|
loadedPlugins: FG_Plugin.Flaschengeist,
|
||||||
plugin: FG_Plugin.Plugin,
|
plugin: FG_Plugin.Plugin,
|
||||||
backend: FG.Backend
|
backend: Backend
|
||||||
) {
|
) {
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
|
if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
|
||||||
|
@ -234,12 +254,6 @@ function loadPlugin(
|
||||||
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,
|
id: plugin.id,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
|
@ -250,17 +264,52 @@ function loadPlugin(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
|
async function loadBaseUrl() {
|
||||||
|
return PersistentStorage.get<string>('baseURL').then((url) => {
|
||||||
|
if (url !== null) api.defaults.baseURL = url;
|
||||||
|
console.log('loaded: ', url, api.defaults.baseURL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Loading backend information
|
||||||
|
* @returns Backend object or null
|
||||||
|
*/
|
||||||
|
async function getBackend() {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<Backend>('/');
|
||||||
|
if (!data || typeof data !== 'object' || !('plugins' in data))
|
||||||
|
throw Error('Invalid backend response received');
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Loading backend', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot file, load all required plugins, check for dependencies
|
||||||
|
*/
|
||||||
|
export default boot(async ({ router, app }) => {
|
||||||
|
await loadBaseUrl();
|
||||||
|
const backend = await getBackend();
|
||||||
|
if (backend === null) {
|
||||||
|
router.isReady().finally(() => {
|
||||||
|
if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
|
||||||
|
else void router.push({ name: 'offline', params: { refresh: 1 } });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
||||||
routes: baseRoutes,
|
routes,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
menuLinks: [],
|
menuLinks: [],
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
outerShortcuts: [],
|
outerShortcuts: [],
|
||||||
widgets: [],
|
widgets: [],
|
||||||
settingWidgets: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
// Wait for all plugins to be loaded
|
// Wait for all plugins to be loaded
|
||||||
const results = await Promise.allSettled(PLUGINS);
|
const results = await Promise.allSettled(PLUGINS);
|
||||||
|
|
||||||
|
@ -278,18 +327,32 @@ export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRa
|
||||||
throw result.value.default.id;
|
throw result.value.default.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (reason) {
|
||||||
|
const id = <string>reason;
|
||||||
|
void router.push({ name: 'error' });
|
||||||
|
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${id}!`,
|
||||||
|
timeout: 10000,
|
||||||
|
progress: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Sort widgets by priority
|
// Sort widgets by priority
|
||||||
/** @todo Remove priority with first beta */
|
/** @todo Remove priority with first beta */
|
||||||
loadedPlugins.widgets.sort(
|
loadedPlugins.widgets.sort(
|
||||||
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
|
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
|
||||||
);
|
);
|
||||||
|
|
||||||
/** @todo Can be cleaned up with first beta */
|
/** @todo Can be cleaned up with first beta */
|
||||||
loadedPlugins.menuLinks.sort((a, b) => {
|
loadedPlugins.menuLinks.sort((a, b) => {
|
||||||
const diff = a.order && b.order ? b.order - a.order : 0;
|
const diff = a.order && b.order ? b.order - a.order : 0;
|
||||||
return diff ? diff : a.title.toString().localeCompare(b.title.toString());
|
return diff ? diff : a.title.toString().localeCompare(b.title.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
return loadedPlugins;
|
// Add loaded routes to router
|
||||||
}
|
loadedPlugins.routes.forEach((route) => router.addRoute(route));
|
||||||
|
|
||||||
|
// save plugins in VM-variable
|
||||||
|
app.provide('flaschengeist', loadedPlugins);
|
||||||
|
});
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
/**
|
import { useMainStore, pinia } from '@flaschengeist/api';
|
||||||
* This boot file installs the global pinia instance
|
import { saveToken } from 'app/api';
|
||||||
*/
|
|
||||||
import { pinia } from '@flaschengeist/api';
|
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
|
|
||||||
|
const store = useMainStore();
|
||||||
|
store.init().finally(() => {
|
||||||
|
store.$subscribe((mutation, state) => {
|
||||||
|
saveToken(state.session?.token);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
class="q-ma-xs"
|
class="q-ma-xs"
|
||||||
title="Löschen"
|
title="Löschen"
|
||||||
style="position: absolute; top: 0; right: 0; z-index: 999"
|
style="position: absolute; top: 0; right: 0; z-index: 999"
|
||||||
@click.stop.prevent="dismiss"
|
@click="dismiss"
|
||||||
/>
|
/>
|
||||||
<q-card-section class="q-pa-xs">
|
<q-card-section class="q-pa-xs">
|
||||||
<div class="text-overline">{{ dateString }}</div>
|
<div class="text-overline">{{ dateString }}</div>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
@click.stop.prevent="accept"
|
@click="accept"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="modelValue.reject"
|
v-if="modelValue.reject"
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
@click.stop.prevent="reject"
|
@click="reject"
|
||||||
/>
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<q-layout view="hHh Lpr lff">
|
<q-layout view="hHh Lpr lff">
|
||||||
<q-header elevated class="bg-primary text-white">
|
<q-header elevated class="bg-primary text-white">
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-btn dense flat round icon="mdi-menu" @click="openMenu(true)" />
|
<q-btn dense flat round icon="mdi-menu" @click="openMenu" />
|
||||||
|
|
||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
<router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit">
|
<router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit">
|
||||||
|
@ -40,14 +40,7 @@
|
||||||
<shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" />
|
<shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" />
|
||||||
</template>
|
</template>
|
||||||
</drag>
|
</drag>
|
||||||
<q-btn
|
<q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" />
|
||||||
v-if="!platform.is.capacitor"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="mdi-exit-to-app"
|
|
||||||
@click="logout()"
|
|
||||||
/>
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
|
@ -56,10 +49,9 @@
|
||||||
side="left"
|
side="left"
|
||||||
bordered
|
bordered
|
||||||
:mini="leftDrawerMini"
|
:mini="leftDrawerMini"
|
||||||
@click.capture="openMenuMini"
|
@click.capture="openMenu"
|
||||||
>
|
>
|
||||||
<!-- Plugins -->
|
<!-- Plugins -->
|
||||||
<q-scroll-area class="fit">
|
|
||||||
<essential-expansion-link
|
<essential-expansion-link
|
||||||
v-for="(entry, index) in mainLinks"
|
v-for="(entry, index) in mainLinks"
|
||||||
:key="'plugin' + index"
|
:key="'plugin' + index"
|
||||||
|
@ -72,29 +64,6 @@
|
||||||
:key="'essential' + index"
|
:key="'essential' + index"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
/>
|
/>
|
||||||
<div v-if="platform.is.capacitor">
|
|
||||||
<q-separator />
|
|
||||||
<q-item clickable tag="a" target="self" @click="logout">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="mdi-exit-to-app" />
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Logout</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
|
||||||
</q-scroll-area>
|
|
||||||
<div class="q-mini-drawer-hide absolute" style="top: 15px; right: -17px">
|
|
||||||
<q-btn
|
|
||||||
dense
|
|
||||||
round
|
|
||||||
unelevated
|
|
||||||
color="accent"
|
|
||||||
icon="mdi-chevron-left"
|
|
||||||
@click="openMenuMini(true)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
@ -137,7 +106,7 @@ export default defineComponent({
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
||||||
const leftDrawer = ref(!Platform.is.mobile);
|
const leftDrawer = ref(!Platform.is.mobile);
|
||||||
const leftDrawerMini = ref(true);
|
const leftDrawerMini = ref(false);
|
||||||
const mainLinks = flaschengeist?.menuLinks || [];
|
const mainLinks = flaschengeist?.menuLinks || [];
|
||||||
const notifications = computed(() => mainStore.notifications.slice().reverse());
|
const notifications = computed(() => mainStore.notifications.slice().reverse());
|
||||||
const polling = ref(NaN);
|
const polling = ref(NaN);
|
||||||
|
@ -150,11 +119,9 @@ export default defineComponent({
|
||||||
void mainStore.getShortcuts();
|
void mainStore.getShortcuts();
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => window.clearInterval(polling.value));
|
onBeforeUnmount(() => window.clearInterval(polling.value));
|
||||||
/*
|
|
||||||
function openMenu(event: { target: HTMLInputElement }) {
|
function openMenu(event: { target: HTMLInputElement }) {
|
||||||
console.log(event.target.nodeName);
|
if (event.target.nodeName === 'DIV') leftDrawerMini.value = false;
|
||||||
if (event.target.nodeName === 'DIV' || event.target.nodeName === 'I')
|
|
||||||
leftDrawerMini.value = false;
|
|
||||||
else {
|
else {
|
||||||
if (!leftDrawer.value || leftDrawerMini.value) {
|
if (!leftDrawer.value || leftDrawerMini.value) {
|
||||||
leftDrawer.value = true;
|
leftDrawer.value = true;
|
||||||
|
@ -165,13 +132,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
function openMenu(value = !leftDrawer.value) {
|
|
||||||
leftDrawer.value = value;
|
|
||||||
}
|
|
||||||
function openMenuMini(value = !leftDrawerMini.value) {
|
|
||||||
leftDrawerMini.value = value;
|
|
||||||
}
|
|
||||||
function logout() {
|
function logout() {
|
||||||
void router.push({ name: 'login', params: { logout: 'logout' } });
|
void router.push({ name: 'login', params: { logout: 'logout' } });
|
||||||
void mainStore.logout();
|
void mainStore.logout();
|
||||||
|
@ -234,14 +195,12 @@ export default defineComponent({
|
||||||
notifications,
|
notifications,
|
||||||
noPermission,
|
noPermission,
|
||||||
openMenu,
|
openMenu,
|
||||||
openMenuMini,
|
|
||||||
remove,
|
remove,
|
||||||
requestPermission,
|
requestPermission,
|
||||||
useNative,
|
useNative,
|
||||||
shortCuts,
|
shortCuts,
|
||||||
addShortcut,
|
addShortcut,
|
||||||
deleteShortcut,
|
deleteShortcut,
|
||||||
platform: Platform,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="row justify-center items-center content-center q-pa-md">
|
||||||
|
<q-card class="col-xs-11 col-sm-8 col-md-6 col-lg-4 justify-center items-center content-center">
|
||||||
|
<q-toolbar class="bg-primary text-white">
|
||||||
|
<q-toolbar-title>Servereinstellung</q-toolbar-title>
|
||||||
|
</q-toolbar>
|
||||||
|
<q-card-section>
|
||||||
|
<q-form class="q-gutter-md" @submit="changeUrl">
|
||||||
|
<q-input v-model="server" filled label="Server" dense />
|
||||||
|
<q-btn dense color="primary" label="Speichern" type="submit" />
|
||||||
|
</q-form>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { notEmpty, PersistentStorage } from '@flaschengeist/api';
|
||||||
|
import { defineComponent, ref } from 'vue';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'PageBackend',
|
||||||
|
setup() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const server = ref(api.defaults.baseURL);
|
||||||
|
|
||||||
|
function changeUrl() {
|
||||||
|
if (server.value) {
|
||||||
|
void PersistentStorage.set('baseURL', server.value).then(() => {
|
||||||
|
console.log('uiuiui');
|
||||||
|
void router.push({ name: 'login' }).then(() => router.go(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
changeUrl,
|
||||||
|
notEmpty,
|
||||||
|
server,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -2,9 +2,9 @@
|
||||||
<q-page
|
<q-page
|
||||||
padding
|
padding
|
||||||
style="grid-auto-rows: 1fr"
|
style="grid-auto-rows: 1fr"
|
||||||
class="row justify-center content-center items-center q-col-gutter-lg"
|
class="fit row justify-around items-start q-col-gutter-sm"
|
||||||
>
|
>
|
||||||
<div v-for="(item, index) in widgets" :key="index" class="full-height col-sm-6 col-xs-12">
|
<div v-for="(item, index) in widgets" :key="index" class="col-4 full-height col-sm-6 col-xs-12">
|
||||||
<component :is="item.widget" />
|
<component :is="item.widget" />
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<q-page padding class="fit row justify-center items-center content-center">
|
<q-page padding class="fit row justify-center items-center content-center">
|
||||||
<q-card class="col-xs-11 col-sm-8 col-md-6 col-lg-4 justify-center items-center content-center">
|
<q-card class="col-xs-11 col-sm-8 col-md-6 col-lg-4 justify-center items-center content-center">
|
||||||
<q-toolbar class="bg-primary text-white">
|
<q-toolbar class="bg-primary text-white">
|
||||||
<q-toolbar-title>{{ backendSetup ? 'Servereinrichtung' : 'Login' }}</q-toolbar-title>
|
<q-toolbar-title> Login </q-toolbar-title>
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
<div v-if="!backendSetup">
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-form @submit="doLogin">
|
<q-form @submit="doLogin">
|
||||||
<q-input
|
<q-input
|
||||||
|
@ -47,101 +47,44 @@
|
||||||
v-if="$q.platform.is.capacitor || $q.platform.is.electron"
|
v-if="$q.platform.is.capacitor || $q.platform.is.electron"
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
:icon="showBackendSetup ? 'mdi-menu-up' : 'mdi-menu-down'"
|
icon="mdi-menu-down"
|
||||||
@click="showBackendSetup = !showBackendSetup"
|
:to="{ name: 'setup_backend' }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<q-slide-transition v-if="$q.platform.is.capacitor">
|
|
||||||
<div v-show="showBackendSetup || backendSetup">
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section>
|
|
||||||
<q-form ref="ServerSettingsForm" class="q-gutter-md" @submit="changeBackend">
|
|
||||||
<div class="text-h6">Backend einrichten</div>
|
|
||||||
<q-input v-model="server" filled label="Server" dense />
|
|
||||||
<q-btn dense color="primary" label="Speichern" type="submit" />
|
|
||||||
</q-form>
|
|
||||||
</q-card-section>
|
|
||||||
</div>
|
|
||||||
</q-slide-transition>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Loading, Notify, useQuasar } from 'quasar';
|
import { Loading, Notify } from 'quasar';
|
||||||
import { api, notEmpty, PersistentStorage, useMainStore } from '@flaschengeist/api';
|
import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||||
import { PasswordInput } from '@flaschengeist/api/components';
|
import { PasswordInput } from '@flaschengeist/api/components';
|
||||||
import { defineComponent, onMounted, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import { useSessionStore } from 'app/api';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'PageLogin',
|
name: 'PageLogin',
|
||||||
components: { PasswordInput },
|
components: { PasswordInput },
|
||||||
props: {
|
setup() {
|
||||||
backendSetup: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const mainRoute = { name: 'dashboard' };
|
const mainRoute = { name: 'dashboard' };
|
||||||
|
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
const sessionStore = useSessionStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const quasar = useQuasar();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (mainStore.session) void router.replace(mainRoute);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Stuff for the real login page */
|
/* Stuff for the real login page */
|
||||||
const userid = ref('');
|
const userid = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const server = ref(api.defaults.baseURL);
|
|
||||||
const showBackendSetup = ref(!!props.backendSetup);
|
|
||||||
|
|
||||||
function changeBackend() {
|
|
||||||
if (server.value) {
|
|
||||||
void PersistentStorage.set('baseURL', server.value).then(() => {
|
|
||||||
showBackendSetup.value = false;
|
|
||||||
void router.go(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
Loading.show({ message: 'Du wirst angemeldet' });
|
Loading.show({ message: 'Du wirst angemeldet' });
|
||||||
const status = await mainStore.login(userid.value, password.value);
|
const status = await mainStore.login(userid.value, password.value);
|
||||||
|
|
||||||
if (status === true) {
|
if (status === true) {
|
||||||
// On capacitor we set the lifetime to at least two weeks to not annoy users.
|
mainStore.user = (await userStore.getUser(userid.value, true)) || undefined;
|
||||||
if (quasar.platform.is.capacitor)
|
const x = router.currentRoute.value.query['redirect'];
|
||||||
await sessionStore.updateSession(14 * 24 * 60 * 60, mainStore.currentSession.token);
|
void router.push(typeof x === 'string' ? { path: x } : mainRoute);
|
||||||
// Redirect user to previous page, if any.
|
|
||||||
// there are different redirects possible:
|
|
||||||
// 1. when explicitely entered
|
|
||||||
// a) http://localhost:8080/ -> should be redirected to mainRoute
|
|
||||||
// b) http://localhost:8080/in/user/settings -> should be redirected to in/user/settings
|
|
||||||
// 2. when automatically logged out:
|
|
||||||
// http://localhost:8080/login?redirect=/in/user/settings
|
|
||||||
// -> should be redirected to in/user/settings
|
|
||||||
var redirect;
|
|
||||||
if (router.currentRoute.value.redirectedFrom) {
|
|
||||||
redirect = router.currentRoute.value.redirectedFrom.path;
|
|
||||||
if (redirect === '/') {
|
|
||||||
redirect = mainRoute;
|
|
||||||
}
|
|
||||||
} else if ('redirect' in router.currentRoute.value.query) {
|
|
||||||
redirect = { path: router.currentRoute.value.query.redirect as string };
|
|
||||||
} else {
|
} else {
|
||||||
redirect = mainRoute;
|
|
||||||
}
|
|
||||||
void router.push(redirect);
|
|
||||||
} else {
|
|
||||||
// Login failed, notify and reset form
|
|
||||||
password.value = '';
|
password.value = '';
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
|
@ -186,14 +129,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changeBackend,
|
|
||||||
doLogin,
|
doLogin,
|
||||||
doReset,
|
doReset,
|
||||||
notEmpty,
|
notEmpty,
|
||||||
password,
|
password,
|
||||||
userid,
|
userid,
|
||||||
server,
|
|
||||||
showBackendSetup,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,18 +10,12 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'login',
|
name: 'login',
|
||||||
path: 'login',
|
path: 'login',
|
||||||
component: () => import('pages/Login.vue'),
|
component: () => import('pages/Login.vue'),
|
||||||
props: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'password_reset',
|
name: 'password_reset',
|
||||||
path: 'reset',
|
path: 'reset',
|
||||||
component: () => import('pages/Reset.vue'),
|
component: () => import('pages/Reset.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/setup-backend',
|
|
||||||
name: 'setup_backend',
|
|
||||||
redirect: { name: 'login', params: { backendSetup: 1 } },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'about_out',
|
name: 'about_out',
|
||||||
path: 'about',
|
path: 'about',
|
||||||
|
@ -59,6 +53,11 @@ const routes: RouteRecordRaw[] = [
|
||||||
name: 'offline',
|
name: 'offline',
|
||||||
component: () => import('pages/Offline.vue'),
|
component: () => import('pages/Offline.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/setup-backend',
|
||||||
|
name: 'setup_backend',
|
||||||
|
component: () => import('pages/Backend.vue'),
|
||||||
|
},
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
{
|
{
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
module.exports = ['@flaschengeist/users'];
|
module.exports = [
|
||||||
|
'@flaschengeist/users',
|
||||||
|
]
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": "@quasar/app-webpack/tsconfig-preset",
|
"extends": "@quasar/app/tsconfig-preset",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|
Loading…
Reference in New Issue