release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
70 changed files with 180 additions and 2168 deletions
Showing only changes of commit d82e025700 - Show all commits

2
.gitignore vendored
View File

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

View File

@ -4,5 +4,6 @@ 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/validators'; export * from './src/utils/validators';
export * from './src/utils/misc'; export * from './src/utils/misc';

View File

@ -1,28 +1,31 @@
import { LocalStorage, SessionStorage } from 'quasar';
import { FG_Plugin } from '@flaschengeist/types'; import { FG_Plugin } from '@flaschengeist/types';
import { fixSession, useSessionStore, useUserStore } from '.'; import { fixSession, useSessionStore, useUserStore } from '.';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { api } from '../internal'; import { api } from '../internal';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { PersistentStorage } from '../utils/persistent';
function loadCurrentSession() { function loadToken() {
const session = LocalStorage.getItem<FG.Session>('session'); return PersistentStorage.get<string>('fg_token');
if (session) session.expires = new Date(session.expires);
return session || undefined;
} }
function loadUser() { function clearToken() {
const user = SessionStorage.getItem<FG.User>('user'); void PersistentStorage.remove('fg_token');
if (user && user.birthday) user.birthday = new Date(user.birthday); }
return user || undefined;
export function saveToken(token?: string) {
if (token === undefined) return clearToken();
PersistentStorage.set('fg_token', token).catch(() =>
console.error('Could not save token to storage')
);
} }
export const useMainStore = defineStore({ export const useMainStore = defineStore({
id: 'main', id: 'main',
state: () => ({ state: () => ({
session: loadCurrentSession(), session: undefined as FG.Session | undefined,
user: loadUser(), user: undefined as FG.User | undefined,
notifications: [] as Array<FG_Plugin.Notification>, notifications: [] as Array<FG_Plugin.Notification>,
shortcuts: [] as Array<FG_Plugin.MenuLink>, shortcuts: [] as Array<FG_Plugin.MenuLink>,
}), }),
@ -45,18 +48,17 @@ export const useMainStore = defineStore({
* Updates session and loads current user * Updates session and loads current user
*/ */
async init() { async init() {
if (this.session) { const sessionStore = useSessionStore();
const sessionStore = useSessionStore(); const userStore = useUserStore();
const session = await sessionStore.getSession(this.session.token);
if (session) { try {
this.session = session; const token = await loadToken();
const userStore = useUserStore(); if (token !== null) {
const user = await userStore.getUser(this.session.userid); this.session = await sessionStore.getSession(token);
if (user) { if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
this.user = user;
SessionStorage.set('user', user);
}
} }
} catch (error) {
console.warn('Could not load token from storage', error);
} }
}, },
@ -141,14 +143,10 @@ export const useMainStore = defineStore({
async setShortcuts() { async setShortcuts() {
await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts); await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
}, },
handleLoggedOut() {
LocalStorage.clear();
this.$patch({ handleLoggedOut() {
session: undefined, this.$reset();
user: undefined, void clearToken();
});
SessionStorage.clear();
}, },
}, },
}); });

View File

@ -0,0 +1,35 @@
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

@ -15,10 +15,10 @@
"lint": "eslint --ext .js,.ts,.vue ./src ./api" "lint": "eslint --ext .js,.ts,.vue ./src ./api"
}, },
"dependencies": { "dependencies": {
"@capacitor/storage": "^1.2.3",
"@flaschengeist/api": "file:./api", "@flaschengeist/api": "file:./api",
"@flaschengeist/users": "^1.0.0-alpha.1", "@flaschengeist/users": "^1.0.0-alpha.1",
"axios": "^0.24.0", "axios": "^0.24.0",
"cordova": "^10.0.0",
"pinia": "^2.0.4", "pinia": "^2.0.4",
"quasar": "^2.3.3" "quasar": "^2.3.3"
}, },
@ -51,11 +51,11 @@
"Firefox esr", "Firefox esr",
"last 6 Chrome versions", "last 6 Chrome versions",
"last 4 Firefox versions", "last 4 Firefox versions",
"last 4 Edge versions", "last 4 Edge versions",
"last 4 Safari versions", "last 4 Safari versions",
"last 4 ChromeAndroid versions", "last 4 ChromeAndroid versions",
"last 1 FirefoxAndroid versions" "last 1 FirefoxAndroid versions"
], ],
"cordova": [ "cordova": [
"iOS >= 13.0", "iOS >= 13.0",
"Android >= 76" "Android >= 76"

View File

@ -75,7 +75,7 @@ module.exports = configure(function (/* ctx */) {
chain.plugin('eslint-webpack-plugin') chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ .use(ESLintPlugin, [{
extensions: [ 'ts', 'js', 'vue' ], extensions: [ 'ts', 'js', 'vue' ],
exclude: 'node_modules' exclude: ['node_modules', 'src-capacitor']
}]) }])
chain.plugin('modify-source-webpack-plugin') chain.plugin('modify-source-webpack-plugin')
.use(ModifySourcePlugin, [{ .use(ModifySourcePlugin, [{

View File

@ -1,10 +1,10 @@
/* 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 {
cordova: true; capacitor: true;
} }
} }

View File

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

View File

@ -0,0 +1,16 @@
{
"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"
}
}

View File

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

View File

@ -1,76 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="de.wu5.flaschengeist" version="2.0.0-alpha.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Flaschengeist</name>
<description>Modular student club administration system</description>
<author email="dev@cordova.apache.org" href="http://cordova.io">
Apache Cordova Team
</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<allow-intent href="market:*" />
<icon density="ldpi" src="res/android/ldpi.png" />
<icon density="mdpi" src="res/android/mdpi.png" />
<icon density="hdpi" src="res/android/hdpi.png" />
<icon density="xhdpi" src="res/android/xhdpi.png" />
<icon density="xxhdpi" src="res/android/xxhdpi.png" />
<icon density="xxxhdpi" src="res/android/xxxhdpi.png" />
<splash density="land-ldpi" src="res/screen/android/splash-land-ldpi.png" />
<splash density="port-ldpi" src="res/screen/android/splash-port-ldpi.png" />
<splash density="land-mdpi" src="res/screen/android/splash-land-mdpi.png" />
<splash density="port-mdpi" src="res/screen/android/splash-port-mdpi.png" />
<splash density="land-hdpi" src="res/screen/android/splash-land-hdpi.png" />
<splash density="port-hdpi" src="res/screen/android/splash-port-hdpi.png" />
<splash density="land-xhdpi" src="res/screen/android/splash-land-xhdpi.png" />
<splash density="port-xhdpi" src="res/screen/android/splash-port-xhdpi.png" />
<splash density="land-xxhdpi" src="res/screen/android/splash-land-xxhdpi.png" />
<splash density="port-xxhdpi" src="res/screen/android/splash-port-xxhdpi.png" />
<splash density="land-xxxhdpi" src="res/screen/android/splash-land-xxxhdpi.png" />
<splash density="port-xxxhdpi" src="res/screen/android/splash-port-xxxhdpi.png" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
<icon height="57" src="res/ios/icon.png" width="57" />
<icon height="114" src="res/ios/icon@2x.png" width="114" />
<icon height="40" src="res/ios/icon-20@2x.png" width="40" />
<icon height="60" src="res/ios/icon-20@3x.png" width="60" />
<icon height="29" src="res/ios/icon-29.png" width="29" />
<icon height="58" src="res/ios/icon-29@2x.png" width="58" />
<icon height="87" src="res/ios/icon-29@3x.png" width="87" />
<icon height="80" src="res/ios/icon-40@2x.png" width="80" />
<icon height="120" src="res/ios/icon-60@2x.png" width="120" />
<icon height="180" src="res/ios/icon-60@3x.png" width="180" />
<icon height="20" src="res/ios/icon-20.png" width="20" />
<icon height="40" src="res/ios/icon-40.png" width="40" />
<icon height="50" src="res/ios/icon-50.png" width="50" />
<icon height="100" src="res/ios/icon-50@2x.png" width="100" />
<icon height="72" src="res/ios/icon-72.png" width="72" />
<icon height="144" src="res/ios/icon-72@2x.png" width="144" />
<icon height="76" src="res/ios/icon-76.png" width="76" />
<icon height="152" src="res/ios/icon-76@2x.png" width="152" />
<icon height="167" src="res/ios/icon-83.5@2x.png" width="167" />
<icon height="1024" src="res/ios/icon-1024.png" width="1024" />
<icon height="48" src="res/ios/icon-24@2x.png" width="48" />
<icon height="55" src="res/ios/icon-27.5@2x.png" width="55" />
<icon height="88" src="res/ios/icon-44@2x.png" width="88" />
<icon height="172" src="res/ios/icon-86@2x.png" width="172" />
<icon height="196" src="res/ios/icon-98@2x.png" width="196" />
<splash src="res/screen/ios/Default@2x~iphone~anyany.png" />
<splash src="res/screen/ios/Default@2x~iphone~comany.png" />
<splash src="res/screen/ios/Default@2x~iphone~comcom.png" />
<splash src="res/screen/ios/Default@3x~iphone~anyany.png" />
<splash src="res/screen/ios/Default@3x~iphone~anycom.png" />
<splash src="res/screen/ios/Default@3x~iphone~comany.png" />
<splash src="res/screen/ios/Default@2x~ipad~anyany.png" />
<splash src="res/screen/ios/Default@2x~ipad~comany.png" />
</platform>
<allow-navigation href="about:*" />
<preference name="SplashMaintainAspectRatio" value="true" />
</widget>

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,4 @@
import { useMainStore, api } from '@flaschengeist/api'; import { useMainStore, api } from '@flaschengeist/api';
import { LocalStorage, Notify } from 'quasar';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import config from 'src/config'; import config from 'src/config';
@ -31,7 +30,8 @@ function minify(o: unknown, cloned = false) {
} }
export default boot(({ router }) => { export default boot(({ router }) => {
api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL; // Persisted value is read in plugins.ts boot file!
api.defaults.baseURL = config.baseURL;
/*** /***
* Intercept requests * Intercept requests
@ -95,17 +95,3 @@ export default boot(({ router }) => {
}); });
export { api }; 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,10 +1,10 @@
import { Notify } from 'quasar'; import { Notify, Platform } from 'quasar';
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import routes from 'src/router/routes'; import routes from 'src/router/routes';
import { AxiosResponse } from 'axios';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { FG_Plugin } from '@flaschengeist/types'; import { FG_Plugin } from '@flaschengeist/types';
import { PersistentStorage } from '@flaschengeist/api';
/**************************************************** /****************************************************
******** Internal area for some magic ************** ******** Internal area for some magic **************
@ -264,16 +264,24 @@ function loadPlugin(
return true; return true;
} }
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 * Loading backend information
* @returns Backend object or null * @returns Backend object or null
*/ */
async function getBackend() { async function getBackend() {
try { try {
const { data }: AxiosResponse<Backend> = await api.get('/'); const { data } = await api.get<Backend>('/');
if (!data || typeof data !== 'object' || !('plugins' in data))
throw Error('Invalid backend response received');
return data; return data;
} catch (e) { } catch (e) {
console.warn(e); console.error('Loading backend', e);
return null; return null;
} }
} }
@ -282,10 +290,13 @@ async function getBackend() {
* Boot file, load all required plugins, check for dependencies * Boot file, load all required plugins, check for dependencies
*/ */
export default boot(async ({ router, app }) => { export default boot(async ({ router, app }) => {
await loadBaseUrl();
const backend = await getBackend(); const backend = await getBackend();
if (!backend || typeof backend !== 'object' || !('plugins' in backend)) { if (backend === null) {
console.log('Backend error'); router.isReady().finally(() => {
router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } })); if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
else void router.push({ name: 'offline', params: { refresh: 1 } });
});
return; return;
} }

View File

@ -1,9 +1,14 @@
import { useMainStore, pinia } from '@flaschengeist/api'; import { useMainStore, pinia } from '@flaschengeist/api';
import { saveToken } from 'app/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(); const store = useMainStore();
void store.init(); store.init().finally(() => {
store.$subscribe((mutation, state) => {
saveToken(state.session?.token);
});
});
}); });

46
src/pages/Backend.vue Normal file
View File

@ -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>

View File

@ -44,25 +44,13 @@
</q-card-section> </q-card-section>
<div class="row justify-end"> <div class="row justify-end">
<q-btn <q-btn
v-if="quasar.platform.is.cordova || quasar.platform.is.electron" v-if="$q.platform.is.capacitor || $q.platform.is.electron"
flat flat
round round
icon="mdi-menu-down" icon="mdi-menu-down"
@click="openServerSettings" :to="{ name: 'setup_backend' }"
/> />
</div> </div>
<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="changeUrl">
<div class="text-h6">Servereinstellung</div>
<q-input v-model="server" filled label="Server" dense />
<q-btn size="xs" 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>
@ -73,34 +61,20 @@ import { Loading, Notify } from 'quasar';
import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api'; import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
import { PasswordInput } from '@flaschengeist/api/components'; import { PasswordInput } from '@flaschengeist/api/components';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { setBaseURL, api } from 'boot/axios';
import { useQuasar } from 'quasar';
export default defineComponent({ export default defineComponent({
name: 'PageLogin', name: 'PageLogin',
components: { PasswordInput }, components: { PasswordInput },
setup() { setup() {
const mainRoute = { name: 'dashboard' };
const mainStore = useMainStore(); const mainStore = useMainStore();
const userStore = useUserStore(); const userStore = useUserStore();
const mainRoute = { name: 'dashboard' };
const router = useRouter(); const router = useRouter();
/* 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<string | undefined>(api.defaults.baseURL);
const visible = ref(false);
const quasar = useQuasar();
function openServerSettings() {
visible.value = !visible.value;
}
function changeUrl() {
if (server.value) {
setBaseURL(server.value);
}
}
async function doLogin() { async function doLogin() {
Loading.show({ message: 'Du wirst angemeldet' }); Loading.show({ message: 'Du wirst angemeldet' });
@ -155,16 +129,11 @@ export default defineComponent({
} }
return { return {
changeUrl,
doLogin, doLogin,
doReset, doReset,
notEmpty, notEmpty,
openServerSettings,
password, password,
server,
userid, userid,
visible,
quasar,
}; };
}, },
}); });

View File

@ -53,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
{ {