Compare commits

..

1 Commits

Author SHA1 Message Date
Ferdinand Thiessen 8ced3e6ce1 Implemented persistent storage when using cordova 2021-11-26 02:56:07 +01:00
37 changed files with 2366 additions and 688 deletions

View File

@ -17,11 +17,11 @@ module.exports = {
project: resolve(__dirname, './tsconfig.json'),
tsconfigRootDir: __dirname,
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: {
browser: true,
browser: true
},
// Rules order is important, please avoid shuffling them
@ -44,7 +44,7 @@ module.exports = {
// https://github.com/prettier/eslint-config-prettier#installation
// usage with Prettier, provided by 'eslint-config-prettier'.
'plugin:prettier/recommended',
'prettier', //'plugin:prettier/recommended'
],
plugins: [
@ -54,6 +54,10 @@ module.exports = {
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
// required to lint *.vue files
'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: {
@ -66,7 +70,7 @@ module.exports = {
__QUASAR_SSR_PWA__: true,
process: true,
Capacitor: true,
chrome: true,
chrome: true
},
// add your custom rules here
@ -76,16 +80,20 @@ module.exports = {
'vue/multi-word-component-names': 'off',
// Rejects on promises should always be of the Error type (and allow empty rejects as well)
'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
// Allow " if ' is contained inside the string, so we can avoid escaping
quotes: ['error', 'single', { avoidEscape: true }],
'prefer-promise-reject-errors': ["error", {"allowEmptyReject": true}],
// Allow " if ' is contained inside the string, so we can avoid escaping
quotes: [
process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'single',
{ avoidEscape: true }
],
// TypeScript, let us be not too strict
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// allow debugger during development only
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

2
.gitignore vendored
View File

@ -18,8 +18,6 @@ yarn.lock
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/android
/src-capacitor/ios
/src-capacitor/node_modules
# BEX related directories and files

View File

@ -3,6 +3,6 @@
module.exports = {
plugins: [
// to edit target browsers: use "browserslist" field in package.json
require('autoprefixer'),
],
};
require('autoprefixer')
]
}

View File

@ -1,16 +0,0 @@
pipeline:
install:
image: node:lts-alpine
commands:
- yarn install
lint:
image: node:lts-alpine
commands:
- yarn lint
build:
image: node:lts-alpine
commands:
- yarn quasar build
branches: [main, develop]

View File

@ -1,7 +1,5 @@
# Flaschengeist (frontend)
![status-badge](http://os-sc.org:8000/api/badges/ferfissimo/flaschengeist-frontend/status.svg)
Modular student club administration system, licensed under the MIT license.
## Installation

View File

@ -44,4 +44,4 @@ export default defineComponent({
};
},
});
</script>
</script>

View File

@ -1,46 +0,0 @@
<template>
<q-avatar>
<slot :avatarURL="avatarURL(modelValue)">
<q-img :src="avatarURL(modelValue)" style="min-width: 100%; min-height: 100%">
<template #error>
<img :src="fallback" style="height: 100%" />
</template>
</q-img>
</slot>
</q-avatar>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { avatarURL } from '@flaschengeist/api';
/**
* Display an avatar for an user
*
* Slots:
* default - scope: {avatarURL}
*/
export default defineComponent({
name: 'UserAvatar',
props: {
modelValue: {
type: [Object, String] as PropType<FG.User | string>,
required: true,
},
showZoom: {
type: Boolean,
default: false,
},
fallback: {
type: String,
default: 'no-image.svg',
},
},
emits: ['error'],
setup() {
return {
avatarURL,
};
},
});
</script>

View File

@ -1,5 +1,4 @@
import IsoDateInput from './IsoDateInput.vue';
import PasswordInput from './PasswordInput.vue';
import UserAvatar from './UserAvatar.vue';
export { IsoDateInput, PasswordInput, UserAvatar };
export { IsoDateInput, PasswordInput };

View File

@ -4,7 +4,5 @@ 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';

View File

@ -1,6 +1,6 @@
{
"license": "MIT",
"version": "1.0.0-alpha.7",
"version": "1.0.0-alpha.4",
"name": "@flaschengeist/api",
"author": "Tim Gröger <flaschengeist@wu5.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist",
@ -8,16 +8,27 @@
"bugs": {
"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",
"peerDependencies": {
"@quasar/app": "^3.2.4",
"@quasar/app": "^3.2.3",
"flaschengeist": "^2.0.0-alpha.1",
"pinia": "^2.0.6"
"pinia": "^2.0.4"
},
"devDependencies": {
"@flaschengeist/types": "^1.0.0-alpha.10",
"@types/node": "^14.18.00",
"typescript": "^4.5.2"
"@flaschengeist/types": "^1.0.0-alpha.5",
"@types/node": "^12.20.37",
"@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": {
"singleQuote": true,

View File

@ -1,24 +1,9 @@
import { SessionStorage } from 'quasar';
import { FG_Plugin } from '@flaschengeist/types';
import { fixSession, useSessionStore, useUserStore } from '.';
import { useSessionStore, useUserStore } from '.';
import { AxiosResponse } from 'axios';
import { api } from '../internal';
import { defineStore } from 'pinia';
import { PersistentStorage } from '../utils/persistent';
function reviveSession() {
return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
}
function clearPersistant() {
void PersistentStorage.remove('fg_session');
}
export function saveSession(session?: FG.Session) {
if (session === undefined) return clearPersistant();
PersistentStorage.set('fg_session', session).catch(() =>
console.error('Could not save token to storage')
);
}
export const useMainStore = defineStore({
id: 'main',
@ -38,10 +23,6 @@ export const useMainStore = defineStore({
if (this.user === undefined) throw 'Not logged in, this should not be called';
return this.user;
},
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 || [];
},
@ -52,29 +33,28 @@ export const useMainStore = defineStore({
* Updates session and loads current user
*/
async init() {
const sessionStore = useSessionStore();
const userStore = useUserStore();
try {
this.session = await reviveSession();
if (this.session !== undefined) {
this.session = await sessionStore.getSession(this.session.token);
if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
if (this.session) {
const sessionStore = useSessionStore();
const session = await sessionStore.getSession(this.session.token);
if (session) {
this.session = session;
const userStore = useUserStore();
const user = await userStore.getUser(this.session.userid);
if (user) {
this.user = user;
SessionStorage.set('user', user);
}
}
} catch (error) {
console.warn('Could not load token from storage', error);
}
},
async login(userid: string, password: string) {
const userStore = useUserStore();
try {
const { data } = await api.post<FG.Session>('/auth', { userid, password });
this.session = fixSession(data);
this.user = await userStore.getUser(data.userid, true);
this.session = data;
this.session.expires = new Date(this.session.expires);
return true;
} catch ({ response }) {
this.handleLoggedOut();
return (<AxiosResponse | undefined>response)?.status || false;
}
},
@ -150,10 +130,14 @@ export const useMainStore = defineStore({
async setShortcuts() {
await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
},
handleLoggedOut() {
this.$reset();
void clearPersistant();
this.$patch({
session: undefined,
user: undefined,
notifications: [],
shortcuts: [],
});
SessionStorage.clear();
},
},
});

View File

@ -3,10 +3,6 @@ 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',
@ -19,13 +15,15 @@ export const useSessionStore = defineStore({
return await api
.get(`/auth/${token}`)
.then(({ data }: AxiosResponse<FG.Session>) => data)
.catch(() => undefined);
.catch(() => null);
},
async getSessions() {
try {
const { data } = await api.get<FG.Session[]>('/auth');
data.forEach(fixSession);
data.forEach((session) => {
session.expires = new Date(session.expires);
});
const mainStore = useMainStore();
const currentSession = data.find((session) => {
@ -57,7 +55,7 @@ export const useSessionStore = defineStore({
async updateSession(lifetime: number, token: string) {
try {
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
fixSession(data);
data.expires = new Date(data.expires);
const mainStore = useMainStore();
if (mainStore.session?.token == data.token) mainStore.session = data;

View File

@ -2,157 +2,81 @@ import { defineStore } from 'pinia';
import { api } from '../internal';
import { isAxiosError, useMainStore } from '.';
export function fixUser(u?: FG.User) {
return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
}
/**
* Check if state is outdated / dirty
* Value is considered outdated after 15 minutes
* @param updated Time of last updated (in milliseconds see Date.now())
* @returns True if outdated, false otherwise
*/
function isDirty(updated: number) {
return Date.now() - updated > 15 * 60 * 1000;
}
export const useUserStore = defineStore({
id: 'users',
state: () => ({
roles: [] as FG.Role[],
users: [] as FG.User[],
permissions: [] as FG.Permission[],
// 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,
_dirty_users: true,
_dirty_roles: true,
}),
getters: {
users(state) {
return state._users.filter((u) => !u.deleted);
},
},
getters: {},
actions: {
/** Simply filter all users by ID */
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) {
const idx = this._users.findIndex((user) => user.userid === userid);
if (force || idx === -1 || isDirty(this._dirty_users)) {
const idx = this.users.findIndex((user) => user.userid === userid);
if (force || this._dirty_users || idx === -1) {
try {
const { data } = await api.get<FG.User>(`/users/${userid}`);
fixUser(data);
if (idx === -1) this._users.push(data);
else this._users[idx] = data;
if (data.birthday) data.birthday = new Date(data.birthday);
if (idx === -1) this.users.push(data);
else this.users[idx] = data;
return data;
} catch (error) {
// Ignore 404, throw all other
if (!isAxiosError(error, 404)) throw error;
}
} 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) {
if (force || isDirty(this._dirty_users)) {
if (force || this._dirty_users) {
const { data } = await api.get<FG.User[]>('/users');
data.forEach(fixUser);
this._users = data;
this._dirty_users = Date.now();
data.forEach((user) => {
if (user.birthday) user.birthday = new Date(user.birthday);
});
this.users = data;
this._dirty_users = false;
}
return this._users;
return this.users;
},
/** Save modifications of user on backend
* @param user Modified user to save
* @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
*/
async updateUser(user: FG.User) {
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;
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) {
const { data } = await api.post<FG.User>('/users', user);
this._users.push(<FG.User>fixUser(data));
this.users.push(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;
async uploadAvatar(user: FG.User, file: string | File) {
const formData = new FormData();
formData.append('file', file);
await api.post(`/users/${user}/avatar`, formData, {
await api.post(`/users/${user.userid}/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`);
async deleteAvatar(user: FG.User) {
await api.delete(`/users/${user.userid}/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');
@ -161,51 +85,39 @@ export const useUserStore = defineStore({
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)) {
if (force || this._dirty_roles) {
const { data } = await api.get<FG.Role[]>('/roles');
this.roles = data;
this._dirty_roles = Date.now();
this._dirty_roles = false;
}
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;
try {
await api.put(`/roles/${role.id}`, role);
} catch (error) {
console.warn(error);
}
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) {
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);
await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`);
this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id));
},
},
});

View File

@ -17,12 +17,12 @@ export function formatDateTime(
return dateTimeFormat.format(date);
}
export function asDate(date?: Date, placeholder = '') {
return date ? formatDateTime(date, true) : placeholder;
export function asDate(date?: Date) {
return date ? formatDateTime(date, true) : '';
}
export function asHour(date?: Date, placeholder = '') {
return date ? formatDateTime(date, false, true) : placeholder;
export function asHour(date?: Date) {
return date ? formatDateTime(date, false, true) : '';
}
export function formatStartEnd(start: Date, end?: Date) {

View File

@ -1,35 +0,0 @@
import { LocalStorage, Platform } from 'quasar';
import { Storage } from '@capacitor/storage';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PersitentTypes = Date | RegExp | number | boolean | string | object;
export class PersistentStorage {
static clear() {
if (Platform.is.capacitor) return Storage.clear();
else return Promise.resolve(LocalStorage.clear());
}
static remove(key: string) {
if (Platform.is.capacitor) return Storage.remove({ key: key });
else return Promise.resolve(LocalStorage.remove(key));
}
static set(key: string, value: PersitentTypes) {
if (Platform.is.capacitor) return Storage.set({ key, value: JSON.stringify(value) });
else return Promise.resolve(LocalStorage.set(key, value));
}
static get<T extends PersitentTypes>(key: string) {
if (Platform.is.capacitor)
return Storage.get({ key }).then((v) =>
v.value === null ? null : (JSON.parse(v.value) as T)
);
else return Promise.resolve(LocalStorage.getItem<T>(key));
}
static keys() {
if (Platform.is.capacitor) return Storage.keys().then((v) => v.keys);
else return Promise.resolve(LocalStorage.getAllKeys());
}
}

View File

@ -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' : ''}`;
}

View File

@ -1,4 +1,6 @@
/* eslint-env node */
module.exports = {
presets: ['@quasar/babel-preset-app'],
};
presets: [
'@quasar/babel-preset-app'
]
}

View File

@ -11,34 +11,32 @@
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
},
"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"
},
"dependencies": {
"@flaschengeist/api": "file:./api",
"@flaschengeist/users": "^1.0.0-alpha.3",
"@flaschengeist/users": "^1.0.0-alpha.1",
"axios": "^0.24.0",
"pinia": "^2.0.6",
"cordova": "^10.0.0",
"pinia": "^2.0.4",
"quasar": "^2.3.3"
},
"devDependencies": {
"@capacitor/core": "^3.3.2",
"@capacitor/storage": "^1.2.3",
"@flaschengeist/types": "^1.0.0-alpha.10",
"@quasar/app": "^3.2.4",
"@quasar/extras": "^1.12.2",
"@types/node": "^14.18.0",
"@flaschengeist/types": "^1.0.0-alpha.6",
"@quasar/app": "^3.2.3",
"@quasar/extras": "^1.12.1",
"@types/node": "^14.17.34",
"@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.16.3",
"@typescript-eslint/eslint-plugin": "^5.5.0",
"@typescript-eslint/parser": "^5.5.0",
"eslint": "^8.4.0",
"@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-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.1.1",
"eslint-webpack-plugin": "^3.1.1",
"modify-source-webpack-plugin": "^3.0.0",
"prettier": "^2.5.1",
"prettier": "^2.4.1",
"typescript": "^4.5.2",
"vuedraggable": "^4.1.0"
},
@ -53,15 +51,14 @@
"Firefox esr",
"last 6 Chrome versions",
"last 4 Firefox versions",
"last 4 Edge versions",
"last 4 Safari versions",
"last 4 Edge versions",
"last 4 Safari versions",
"last 4 ChromeAndroid versions",
"last 1 FirefoxAndroid versions"
],
],
"cordova": [
"iOS >= 13.0",
"Android >= 76",
"ChromeAndroid >= 76"
"Android >= 76"
]
},
"engines": {

View File

@ -8,9 +8,10 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */
const ESLintPlugin = require('eslint-webpack-plugin');
const { ModifySourcePlugin } = require('modify-source-webpack-plugin');
const { configure } = require('quasar/wrappers');
const ESLintPlugin = require('eslint-webpack-plugin')
const { ModifySourcePlugin } = require('modify-source-webpack-plugin')
const { configure } = require('quasar/wrappers')
module.exports = configure(function (/* ctx */) {
return {
@ -21,7 +22,7 @@ module.exports = configure(function (/* ctx */) {
enabled: true,
files: './src/**/*.{ts,tsx,js,jsx,vue}',
},
},
}
},
// https://quasar.dev/quasar-cli/prefetch-feature
@ -30,7 +31,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// 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
css: ['app.scss'],
@ -44,7 +45,7 @@ module.exports = configure(function (/* ctx */) {
// 'material-icons',
'mdi-v6',
// 'themify',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
],
@ -55,6 +56,7 @@ module.exports = configure(function (/* ctx */) {
// transpile: false,
// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true.
@ -69,38 +71,33 @@ module.exports = configure(function (/* ctx */) {
// https://quasar.dev/quasar-cli/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpack(chain) {
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
{
extensions: ['ts', 'js', 'vue'],
exclude: ['node_modules', 'src-capacitor'],
},
]);
chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
{
chainWebpack (chain) {
chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{
extensions: [ 'ts', 'js', 'vue' ],
exclude: 'node_modules'
}])
chain.plugin('modify-source-webpack-plugin')
.use(ModifySourcePlugin, [{
rules: [
{
test: /plugins\.ts$/,
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(',')
);
},
},
],
},
]);
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({
snapshot: {
managedPaths: [],
},
});
},
managedPaths: []
}
})
}
},
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
@ -108,7 +105,7 @@ module.exports = configure(function (/* ctx */) {
https: false,
port: 8080,
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
@ -120,8 +117,8 @@ module.exports = configure(function (/* ctx */) {
loadingBar: {
position: 'top',
color: 'warning',
size: '5px',
},
size: '5px'
}
},
// For special cases outside of where the auto-import stategy can have an impact
@ -132,7 +129,14 @@ module.exports = configure(function (/* ctx */) {
// directives: [],
// Quasar plugins
plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
plugins: [
'LocalStorage',
'SessionStorage',
'Dialog',
'Loading',
'Notify',
'LoadingBar'
]
},
// animations: 'all', // --- includes all animations
@ -141,7 +145,7 @@ module.exports = configure(function (/* ctx */) {
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: {
pwa: false,
pwa: false
},
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
@ -160,20 +164,20 @@ module.exports = configure(function (/* ctx */) {
{
src: 'flaschengeist-logo.svg',
sizes: 'any',
type: 'image/svg+xml',
type: 'image/svg+xml'
},
{
src: 'favicon-128x128.png',
sizes: '128x128',
type: 'image/png',
type: 'image/png'
},
{
src: 'favicon-256x256.png',
sizes: '256x256',
type: 'image/png',
type: 'image/png'
},
],
},
]
}
},
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
@ -183,7 +187,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true,
hideSplashscreen: true
},
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
@ -192,11 +196,13 @@ module.exports = configure(function (/* ctx */) {
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
@ -204,16 +210,16 @@ module.exports = configure(function (/* ctx */) {
builder: {
// https://www.electron.build/configuration/configuration
appId: 'flaschengeist-frontend',
appId: 'flaschengeist-frontend'
},
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
nodeIntegration: true,
extendWebpack(/* cfg */) {
extendWebpack (/* cfg */) {
// do something with Electron main process Webpack cfg
// chainWebpack also available besides this extendWebpack
},
},
};
}
}
}
});

View File

@ -1,10 +0,0 @@
{
"appId": "dev.flaschengeist",
"appName": "flaschengeist-frontend",
"bundledWebRuntime": false,
"npmClient": "yarn",
"webDir": "www",
"ios": {
"allowsLinkPreview": false
}
}

View File

@ -1,16 +0,0 @@
{
"name": "flaschengeist",
"version": "2.0.0-alpha.1",
"description": "Modular student club administration system",
"author": "Tim Gröger <flaschengeist@wu5.de>",
"private": true,
"dependencies": {
"@capacitor/android": "^3.3.2",
"@capacitor/app": "^1.0.0",
"@capacitor/cli": "^3.0.0",
"@capacitor/core": "^3.0.0",
"@capacitor/ios": "^3.0.0-beta.0",
"@capacitor/splash-screen": "^1.0.0",
"@capacitor/storage": "^1.2.3"
}
}

8
src-cordova/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
# Generated by package manager
node_modules/
# Generated by Cordova
/plugins/
/platforms/

24
src-cordova/config.xml Normal file
View File

@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="de.wu5.flaschengeist" version="2.0.0-alpha.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>flaschengeist-frontend</name>
<description>Modular student club administration system</description>
<author email="dev@cordova.apache.org" href="http://cordova.io">
Apache Cordova Team
</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<allow-intent href="market:*" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
</platform>
<allow-navigation href="about:*" />
</widget>

View File

@ -5,6 +5,6 @@ import 'quasar/dist/types/feature-flag';
declare module 'quasar/dist/types/feature-flag' {
interface QuasarFeatureFlags {
capacitor: true;
cordova: true;
}
}

1769
src-cordova/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
src-cordova/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "dev.flaschengeist",
"displayName": "flaschengeist-frontend",
"version": "1.0.0",
"description": "A sample Apache Cordova application that responds to the deviceready event.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"ecosystem:cordova"
],
"author": "Apache Cordova Team",
"license": "Apache-2.0",
"devDependencies": {
"cordova-android": "^9.1.0",
"cordova-ios": "^6.1.1",
"cordova-plugin-nativestorage": "^2.3.2",
"cordova-plugin-whitelist": "^1.3.4"
},
"cordova": {
"plugins": {
"cordova-plugin-whitelist": {},
"cordova-plugin-nativestorage": {}
},
"platforms": [
"android"
]
}
}

View File

@ -1,31 +1,19 @@
/**
* This boot file registers interceptors for axios
*/
import { useMainStore, api } from '@flaschengeist/api';
import { LocalStorage, Notify } from 'quasar';
import { AxiosError } from 'axios';
import { boot } from 'quasar/wrappers';
import config from 'src/config';
import { clone } from '@flaschengeist/api';
/**
* 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);
function minify(o: unknown, cloned = false) {
if (!cloned) o = clone(o);
if (typeof entity === 'object') {
const obj = entity as { [index: string]: unknown };
if (typeof o === 'object') {
const obj = o 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' &&
@ -39,12 +27,11 @@ function minify(entity: unknown, cloned = false) {
}
return obj;
}
return entity;
return o;
}
export default boot(({ router }) => {
// Persisted value is read in plugins.ts boot file!
if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
/***
* Intercept requests
@ -54,9 +41,7 @@ export default boot(({ router }) => {
api.interceptors.request.use((config) => {
const store = useMainStore();
if (store.session?.token) {
config.headers = Object.assign(config.headers || {}, {
Authorization: `Bearer ${store.session.token}`,
});
config.headers = { Authorization: 'Bearer ' + store.session.token };
}
// Minify JSON requests
if (
@ -94,11 +79,12 @@ export default boot(({ router }) => {
query: { redirect: next },
});
} else if (e.response && e.response.status == 401) {
store.handleLoggedOut();
if (current.name != 'login') {
void store.handleLoggedOut();
if (current.name !== 'login') {
await router.push({
name: 'login',
query: { redirect: current.fullPath },
params: { logout: 'logout' },
query: { redirect: current.path },
});
}
}
@ -107,3 +93,19 @@ export default boot(({ router }) => {
}
);
});
export { api };
export const setBaseURL = (url: string) => {
LocalStorage.set('baseURL', url);
api.defaults.baseURL = url;
Notify.create({
message: 'Serveraddresse gespeichert',
position: 'bottom',
caption: `${url}`,
color: 'positive',
});
setTimeout(() => {
window.location.reload();
}, 5000);
};

View File

@ -1,87 +0,0 @@
/**
* This boot file initalizes the store from persistent storage and load all plugins
*/
import {
PersistentStorage,
api,
isAxiosError,
saveSession,
useMainStore,
} from '@flaschengeist/api';
import { Notify, Platform } from 'quasar';
import { loadPlugins } from './plugins';
import { boot } from 'quasar/wrappers';
import routes from 'src/router/routes';
async function loadBaseUrl() {
try {
const url = await PersistentStorage.get<string>('baseURL');
if (url !== null) api.defaults.baseURL = url;
} catch (e) {
console.warn('Could not load BaseURL', e);
}
}
class BackendError extends Error {}
/**
* Loading backend information
* @returns Backend object or null
*/
async function getBackend() {
const { data } = await api.get<FG.Backend>('/');
if (!data || typeof data !== 'object' || !('plugins' in data))
throw new BackendError('Invalid backend response received');
return data;
}
/**
* Boot file for loading baseURL + Session from PersistentStorage + loading and initializing all plugins
*/
export default boot(async ({ app, router }) => {
const store = useMainStore();
// FIRST(!) get the base URL
await loadBaseUrl();
// Init the store, load current session and user, if available
try {
await store.init();
} finally {
// Any changes on the session is written back to the persistent store
store.$subscribe((mutation, state) => {
saveSession(state.session);
});
}
// Load all plugins
try {
// Fetch backend data
const backend = await getBackend();
// Load enabled plugins
const flaschengeist = await loadPlugins(backend, routes);
// Add loaded routes to router
flaschengeist.routes.forEach((route) => router.addRoute(route));
// save plugins in VM-variable
app.provide('flaschengeist', flaschengeist);
} catch (error) {
// Handle errors from loading the backend information
if (error instanceof BackendError || isAxiosError(error)) {
router.isReady().finally(() => {
if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
else void router.push({ name: 'offline', params: { refresh: 1 } });
});
} else if (typeof error === 'string') {
// Handle plugin not found errors
void router.push({ name: 'error' });
Notify.create({
type: 'negative',
message: `Fehler beim Laden: Bitte wende dich an den Admin (${error})!`,
timeout: 10000,
progress: true,
});
} else {
console.error('Unknown error in init.ts:', error);
}
}
});

14
src/boot/loading.ts Normal file
View File

@ -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
});
});

View File

@ -1,33 +1,42 @@
/**
* This boot file registers login / authentification related axios interceptors
*/
import { useMainStore, hasPermissions } from '@flaschengeist/api';
import { boot } from 'quasar/wrappers';
import { RouteRecord } from 'vue-router';
export default boot(({ router }) => {
/**
* Login guard
* Check if user tries to access the secured area and validates token
*/
router.beforeEach((to, from) => {
router.beforeResolve((to, from, next) => {
const store = useMainStore();
// Skip loops
if (to.name == 'login' && from.name == 'login') return false;
if (to.path == from.path) return next();
// Secured area '/in/...' requires to be authenticated
if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
store.handleLoggedOut();
return { name: 'login' };
if (to.path.startsWith('/main')) {
// Secured area (LOGIN REQUIRED)
// Check login is ok
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;
});
});

View File

@ -1,5 +1,10 @@
import { FG_Plugin } from '@flaschengeist/types';
import { Notify } from 'quasar';
import { api } from 'src/boot/axios';
import { boot } from 'quasar/wrappers';
import routes from 'src/router/routes';
import { AxiosResponse } from 'axios';
import { RouteRecordRaw } from 'vue-router';
import { FG_Plugin } from '@flaschengeist/types';
/****************************************************
******** Internal area for some magic **************
@ -21,6 +26,21 @@ const PLUGINS = <Array<Promise<ImportPlgn>>>[
/*INSERT_PLUGIN_LIST*/
];
interface BackendPlugin {
permissions: string[];
version: string;
}
interface BackendPlugins {
[key: string]: BackendPlugin;
}
interface Backend {
plugins: BackendPlugins;
version: string;
}
export { Backend };
// Handle Notifications
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(
loadedPlugins: FG_Plugin.Flaschengeist,
plugin: FG_Plugin.Plugin,
backend: FG.Backend
backend: Backend
) {
// Check if already loaded
if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
@ -244,9 +264,33 @@ function loadPlugin(
return true;
}
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
/**
* Loading backend information
* @returns Backend object or null
*/
async function getBackend() {
try {
const { data }: AxiosResponse<Backend> = await api.get('/');
return data;
} catch (e) {
console.warn(e);
return null;
}
}
/**
* Boot file, load all required plugins, check for dependencies
*/
export default boot(async ({ router, app }) => {
const backend = await getBackend();
if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
console.log('Backend error');
router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
return;
}
const loadedPlugins: FG_Plugin.Flaschengeist = {
routes: baseRoutes,
routes,
plugins: [],
menuLinks: [],
shortcuts: [],
@ -254,35 +298,50 @@ export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRa
widgets: [],
};
// Wait for all plugins to be loaded
const results = await Promise.allSettled(PLUGINS);
try {
// Wait for all plugins to be loaded
const results = await Promise.allSettled(PLUGINS);
// Check if loaded successfully
results.forEach((result) => {
if (result.status === 'rejected') {
throw <string>result.reason;
} else {
if (
!(
validatePlugin(result.value.default) &&
loadPlugin(loadedPlugins, result.value.default, backend)
// Check if loaded successfully
results.forEach((result) => {
if (result.status === 'rejected') {
throw <string>result.reason;
} else {
if (
!(
validatePlugin(result.value.default) &&
loadPlugin(loadedPlugins, result.value.default, backend)
)
)
)
throw result.value.default.id;
}
});
throw result.value.default.id;
}
});
} catch (reason) {
const id = <string>reason;
void router.push({ name: 'error' });
Notify.create({
type: 'negative',
message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${id}!`,
timeout: 10000,
progress: true,
});
}
// Sort widgets by priority
/** @todo Remove priority with first beta */
loadedPlugins.widgets.sort(
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
);
/** @todo Can be cleaned up with first beta */
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());
});
return loadedPlugins;
}
// Add loaded routes to router
loadedPlugins.routes.forEach((route) => router.addRoute(route));
// save plugins in VM-variable
app.provide('flaschengeist', loadedPlugins);
});

View File

@ -1,9 +1,50 @@
/**
* This boot file installs the global pinia instance
*/
import { pinia } from '@flaschengeist/api';
import { LocalStorage } from 'quasar';
import { boot } from 'quasar/wrappers';
import { useMainStore, pinia } from '@flaschengeist/api';
function isCordova() {
return window.hasOwnProperty('cordova');
}
function error() {
console.debug('NativeStorage Error');
}
export default boot(({ app }) => {
app.use(pinia);
document.addEventListener('deviceready', () => {
const store = useMainStore();
// Load main state from storage
if (isCordova()) {
// is cordova, so use Native Storage
window.NativeStorage.keys((keys) => {
// Check if key is set
if (keys.includes('main_store'))
window.NativeStorage.getItem<typeof store.$state>(
'main_store',
(v) => {
// Check is object, then patch it
if (typeof v === 'object') store.$patch(v);
},
error
);
}, error);
} else {
// is not cordova, so use LocalStorage
store.$patch(LocalStorage.getItem<typeof store.$state>('main_store') || {});
}
// Subscript to save changes of state to storage
store.$subscribe(
(mutation, state) => {
if (isCordova()) window.NativeStorage.setItem('main_store', state, () => undefined, error);
else LocalStorage.set('main_store', state);
},
{ deep: true }
);
void store.init();
});
});

67
src/cordova.d.ts vendored Normal file
View File

@ -0,0 +1,67 @@
/**
* @name Native Storage
* @premier nativestorage
* @description Native storage of variables in Android and iOS
*
* @usage
* ```typescript
* import { NativeStorage } from '@ionic-native/native-storage/ngx';
*
* constructor(private nativeStorage: NativeStorage) { }
*
* ...
*
* this.nativeStorage.setItem('myitem', {property: 'value', anotherProperty: 'anotherValue'})
* .then(
* () => console.log('Stored item!'),
* error => console.error('Error storing item', error)
* );
*
* this.nativeStorage.getItem('myitem')
* .then(
* data => console.log(data),
* error => console.error(error)
* );
* ```
*/
export {};
declare class NativeStorageOriginal {
/**
* Initialises shared storage with the suite name when using app groups in iOS
* @param reference {string}
*/
initWithSuiteName(reference: string, success: () => void, error: () => void): void;
/**
* Stores a value
* @param reference {string}
* @param value
*/
setItem(reference: string, value: unknown, success: () => void, error: () => void): void;
/**
* Gets a stored item
* @param reference {string}
*/
getItem<T = unknown>(reference: string, success: (obj: T) => void, error: () => void): void;
/**
* Retrieving all keys
*/
keys(success: (keys: string[]) => void, error: () => void): void;
/**
* Removes a single stored item
* @param reference {string}
*/
remove(reference: string, success: () => void, error: () => void): void;
/**
* Removes all stored values.
*/
clear(success: () => void, error: () => void): void;
}
declare const NativeStorage: NativeStorageOriginal;
declare global {
interface Window {
NativeStorage: NativeStorageOriginal;
}
}

View File

@ -40,14 +40,7 @@
<shortcut-link :shortcut="element" context @delete-shortcut="deleteShortcut" />
</template>
</drag>
<q-btn
v-if="!platform.is.capacitor"
flat
round
dense
icon="mdi-exit-to-app"
@click="logout()"
/>
<q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" />
</q-toolbar>
</q-header>
@ -71,18 +64,6 @@
:key="'essential' + index"
:entry="entry"
/>
<div v-if="platform.is.capacitor">
<q-separator />
<q-item clickable tag="a" target="self" @click="logout">
<q-item-section avatar>
<q-icon name="mdi-exit-to-app" />
</q-item-section>
<q-item-section>
<q-item-label>Logout</q-item-label>
</q-item-section>
</q-item>
</div>
</q-drawer>
<q-page-container>
<router-view />
@ -220,7 +201,6 @@ export default defineComponent({
shortCuts,
addShortcut,
deleteShortcut,
platform: Platform,
};
},
});

View File

@ -2,64 +2,63 @@
<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-toolbar class="bg-primary text-white">
<q-toolbar-title>{{ backendSetup ? 'Servereinrichtung' : 'Login' }}</q-toolbar-title>
<q-toolbar-title> Login </q-toolbar-title>
</q-toolbar>
<div v-if="!backendSetup">
<q-card-section>
<q-form @submit="doLogin">
<q-input
v-model="userid"
class="q-pa-md"
filled
label="Benutzername oder E-Mail"
autocomplete="username"
:rules="[notEmpty]"
tabindex="1"
/>
<password-input
v-model="password"
class="q-px-md q-pt-md q-pb-none"
filled
label="Passwort"
autocomplete="cureent-password"
:rules="[notEmpty]"
tabindex="2"
/>
<div class="full-width row justify-end q-px-md">
<q-btn
label="Passwort vergessen?"
type="a"
color="secondary"
tabindex="4"
flat
dense
size="sm"
@click="doReset"
/>
</div>
<div class="full-width row justify-end q-px-md q-pt-md">
<q-btn label="Login" type="submit" color="primary" tabindex="3" />
</div>
</q-form>
</q-card-section>
<div class="row justify-end">
<q-btn
v-if="$q.platform.is.capacitor || $q.platform.is.electron"
flat
round
:icon="showBackendSetup ? 'mdi-menu-up' : 'mdi-menu-down'"
@click="showBackendSetup = !showBackendSetup"
<q-card-section>
<q-form @submit="doLogin">
<q-input
v-model="userid"
class="q-pa-md"
filled
label="Benutzername oder E-Mail"
autocomplete="username"
:rules="[notEmpty]"
tabindex="1"
/>
</div>
<password-input
v-model="password"
class="q-px-md q-pt-md q-pb-none"
filled
label="Passwort"
autocomplete="cureent-password"
:rules="[notEmpty]"
tabindex="2"
/>
<div class="full-width row justify-end q-px-md">
<q-btn
label="Passwort vergessen?"
type="a"
color="secondary"
tabindex="4"
flat
dense
size="sm"
@click="doReset"
/>
</div>
<div class="full-width row justify-end q-px-md q-pt-md">
<q-btn label="Login" type="submit" color="primary" tabindex="3" />
</div>
</q-form>
</q-card-section>
<div class="row justify-end">
<q-btn
v-if="quasar.platform.is.cordova || quasar.platform.is.electron"
flat
round
icon="mdi-menu-down"
@click="openServerSettings"
/>
</div>
<q-slide-transition v-if="$q.platform.is.capacitor">
<div v-show="showBackendSetup || backendSetup">
<q-slide-transition v-if="quasar.platform.is.cordova || quasar.platform.is.electron">
<div v-show="visible">
<q-separator />
<q-card-section>
<q-form ref="ServerSettingsForm" class="q-gutter-md" @submit="changeBackend">
<div class="text-h6">Backend einrichten</div>
<q-form ref="ServerSettingsForm" class="q-gutter-md" @submit="changeUrl">
<div class="text-h6">Servereinstellung</div>
<q-input v-model="server" filled label="Server" dense />
<q-btn dense color="primary" label="Speichern" type="submit" />
<q-btn size="xs" dense color="primary" label="Speichern" type="submit" />
</q-form>
</q-card-section>
</div>
@ -70,45 +69,36 @@
<script lang="ts">
import { useRouter } from 'vue-router';
import { Loading, Notify, useQuasar } from 'quasar';
import { api, notEmpty, PersistentStorage, useMainStore } from '@flaschengeist/api';
import { Loading, Notify } from 'quasar';
import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
import { PasswordInput } from '@flaschengeist/api/components';
import { defineComponent, onMounted, ref } from 'vue';
import { useSessionStore } from 'app/api';
import { defineComponent, ref } from 'vue';
import { setBaseURL, api } from 'boot/axios';
import { useQuasar } from 'quasar';
export default defineComponent({
name: 'PageLogin',
components: { PasswordInput },
props: {
backendSetup: {
type: Boolean,
default: false,
},
},
setup(props) {
const mainRoute = { name: 'dashboard' };
setup() {
const mainStore = useMainStore();
const sessionStore = useSessionStore();
const userStore = useUserStore();
const mainRoute = { name: 'dashboard' };
const router = useRouter();
const quasar = useQuasar();
onMounted(() => {
if (mainStore.session) void router.replace(mainRoute);
});
/* Stuff for the real login page */
const userid = ref('');
const password = ref('');
const server = ref(api.defaults.baseURL);
const showBackendSetup = ref(!!props.backendSetup);
const server = ref<string | undefined>(api.defaults.baseURL);
const visible = ref(false);
const quasar = useQuasar();
function changeBackend() {
function openServerSettings() {
visible.value = !visible.value;
}
function changeUrl() {
if (server.value) {
void PersistentStorage.set('baseURL', server.value).then(() => {
showBackendSetup.value = false;
void router.go(0);
});
setBaseURL(server.value);
}
}
@ -117,18 +107,10 @@ export default defineComponent({
const status = await mainStore.login(userid.value, password.value);
if (status === true) {
// On capacitor we set the lifetime to at least two weeks to not annoy users.
if (quasar.platform.is.capacitor)
await sessionStore.updateSession(14 * 24 * 60 * 60, mainStore.currentSession.token);
// Redirect user to previous page, if any.
const redirect =
router.currentRoute.value.redirectedFrom || 'redirect' in router.currentRoute.value.query
? { path: router.currentRoute.value.query.redirect as string }
: mainRoute;
void router.push(redirect);
mainStore.user = (await userStore.getUser(userid.value, true)) || undefined;
const x = router.currentRoute.value.query['redirect'];
void router.push(typeof x === 'string' ? { path: x } : mainRoute);
} else {
// Login failed, notify and reset form
password.value = '';
if (status === 401) {
Notify.create({
@ -173,14 +155,16 @@ export default defineComponent({
}
return {
changeBackend,
changeUrl,
doLogin,
doReset,
notEmpty,
openServerSettings,
password,
userid,
server,
showBackendSetup,
userid,
visible,
quasar,
};
},
});

View File

@ -10,18 +10,12 @@ const routes: RouteRecordRaw[] = [
name: 'login',
path: 'login',
component: () => import('pages/Login.vue'),
props: true,
},
{
name: 'password_reset',
path: 'reset',
component: () => import('pages/Reset.vue'),
},
{
path: '/setup-backend',
name: 'setup_backend',
redirect: { name: 'login', params: { backendSetup: 1 } },
},
{
name: 'about_out',
path: 'about',

View File

@ -1 +1,3 @@
module.exports = ['@flaschengeist/users'];
module.exports = [
'@flaschengeist/users',
]