Transfered files from flaschengeist main tree

This commit is contained in:
Ferdinand Thiessen 2021-05-25 15:58:01 +02:00
parent a108cbdbbd
commit 5a5bbd0dbd
16 changed files with 1023 additions and 0 deletions

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"private": true,
"license": "MIT",
"version": "1.0.0-alpha.1",
"name": "@flaschengeist/users",
"author": "Ferdinand <rpm@fthiessen.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist",
"description": "Flaschengeist users plugin",
"bugs": {
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
},
"repository": {
"type": "git",
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-users"
},
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"valid": "tsc --noEmit",
"pretty": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'"
},
"devDependencies": {
"prettier": "^2.3.0",
"typescript": "^4.2.4",
},
"peerDependencies": {
"@flaschengeist/types": "^0.0.1-alpha.1",
"@flaschengeist/api": "^1.0.0-alpha.1",
"pinia": "^2.0.0-alpha.18",
"quasar": "^2.0.0-beta.17",
"@quasar/app": "^3.0.0-beta.25"
},
"prettier": {
"singleQuote": true,
"semi": true,
"printWidth": 100,
"arrowParens": "always"
}
}

View File

@ -0,0 +1,39 @@
<template>
<q-card class="12">
<q-card-section class="fit row justify-start content-center items-center">
<div class="col-xs-12 col-sm-6 text-center text-h6">Neues Mitglied</div>
</q-card-section>
<q-card-section>
<MainUserSettings :user="user" :new-user="true" @update:user="setUser" />
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import MainUserSettings from './settings/MainUserSettings.vue';
import { useUserStore } from '@flaschengeist/api';
export default defineComponent({
name: 'NewUser',
components: { MainUserSettings },
setup() {
const userStore = useUserStore();
const user = ref<FG.User>({
userid: '',
display_name: '',
firstname: '',
lastname: '',
mail: '',
roles: [],
});
async function setUser(value: FG.User) {
await userStore.createUser(value);
}
return { user, setUser };
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,40 @@
<template>
<q-card class="col-12">
<q-card-section class="fit row justify-start content-center items-center">
<div class="col-xs-12 col-sm-6 text-center text-h6">Benutzereinstellungen</div>
<div class="col-xs-12 col-sm-6 q-pa-sm">
<UserSelector v-model="user" />
</div>
</q-card-section>
<MainUserSettings :user="user" @update:user="updateUser" />
</q-card>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import MainUserSettings from './settings/MainUserSettings.vue';
import UserSelector from './UserSelector.vue';
import { useMainStore, useUserStore } from '@flaschengeist/api';
export default defineComponent({
name: 'UpdateUser',
components: { UserSelector, MainUserSettings },
setup() {
const mainStore = useMainStore();
const userStore = useUserStore();
const user = ref(mainStore.currentUser);
async function updateUser(value: FG.User) {
await userStore.updateUser(value);
user.value = value
}
return {
user,
updateUser,
};
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,43 @@
<template>
<q-select
v-model="selected"
filled
:label="label"
:options="users"
option-label="display_name"
option-value="userid"
map-options
/>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, onBeforeMount } from 'vue';
import { useUserStore } from '@flaschengeist/api';
export default defineComponent({
name: 'UserSelector',
props: {
label: { type: String, default: 'Benutzer' },
modelValue: { default: undefined, type: Object as PropType<FG.User | undefined> },
},
emits: { 'update:modelValue': (user: FG.User) => !!user },
setup(props, { emit }) {
const userStore = useUserStore();
onBeforeMount(() => {
void userStore.getUsers(false);
});
const users = computed(() => userStore.users);
const selected = computed({
get: () => props.modelValue,
set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined),
});
return {
selected,
users,
};
},
});
</script>

71
src/components/Widget.vue Normal file
View File

@ -0,0 +1,71 @@
<template>
<q-card style="text-align: center">
<q-card-section class="row justify-center content-stretch">
<div v-if="avatar" class="col-4">
<div style="width: 100%; padding-bottom: 100%; position: relative">
<q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
<img :src="avatarLink" :onerror="error" />
</q-avatar>
</div>
</div>
<div class="col-8">
<span class="text-h6">Hallo {{ name }}</span
><br />
<span v-if="hasBirthday">Herzlichen Glückwunsch zum Geburtstag!<br /></span>
<span v-if="birthday.length > 0"
>Heute <span v-if="birthday.length === 1">hat </span><span v-else>haben </span
><span v-for="(user, index) in birthday" :key="index"
>{{ user.display_name }}<span v-if="index < birthday.length - 1">, </span></span
>
Geburtstag.</span
>
<span v-else>Heute stehen keine Geburtstage an</span>
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import { useMainStore, useUserStore } from '@flaschengeist/api';
import { computed, defineComponent, onMounted, ref } from 'vue';
export default defineComponent({
name: 'Greeting',
setup() {
const mainStore = useMainStore();
const userStore = useUserStore();
// Ensure users are loaded,so we can query birthdays
onMounted(() => userStore.getUsers(false));
const avatar = ref(true);
const name = ref(mainStore.currentUser.firstname);
const avatarLink = ref(mainStore.currentUser.avatar_url);
function error() {
avatar.value = false;
}
function userHasBirthday(user: FG.User) {
const today = new Date();
return (
user.birthday &&
user.birthday.getMonth() === today.getMonth() &&
user.birthday.getDate() === today.getDate()
);
}
const hasBirthday = computed(() => {
return userHasBirthday(mainStore.currentUser);
});
const birthday = computed(() =>
userStore.users
.filter(userHasBirthday)
.filter((user) => user.userid !== mainStore.currentUser.userid)
);
return { avatar, avatarLink, error, name, hasBirthday, birthday };
},
});
</script>

View File

@ -0,0 +1,210 @@
<template>
<q-form @submit="save" @reset="reset">
<q-card-section class="fit row justify-start content-center items-center">
<q-input
v-model="userModel.firstname"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Vorname"
:rules="[notEmpty]"
autocomplete="given-name"
filled
/>
<q-input
v-model="userModel.lastname"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Nachname"
:rules="[notEmpty]"
autocomplete="family-name"
filled
/>
<q-input
v-model="userModel.display_name"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Angezeigter Name"
:rules="[notEmpty]"
autocomplete="nickname"
filled
/>
<q-input
v-model="userModel.mail"
class="col-xs-12 col-sm-6 q-pa-sm"
label="E-Mail"
:rules="[isEmail, notEmpty]"
autocomplete="email"
filled
/>
<q-input
v-model="userModel.userid"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Benutzername"
:readonly="!newUser"
:rules="newUser ? [isFreeUID, notEmpty] : []"
autocomplete="username"
filled
/>
<q-select
v-model="userModel.roles"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Rollen"
filled
multiple
use-chips
:readonly="!canSetRoles"
:options="allRoles"
option-label="name"
option-value="name"
/>
<IsoDateInput
v-model="userModel.birthday"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Geburtstag"
autocomplete="bday"
/>
<q-file
v-model="avatar"
class="col-xs-12 col-sm-6 q-pa-sm"
filled
label="Avatar"
accept=".jpg, image/*"
max-file-size="204800"
hint="Bilddateien, max. 200 KiB"
@rejected="onAvatarRejected"
>
<template #append>
<q-icon name="mdi-file-image" @click.stop />
</template>
</q-file>
</q-card-section>
<q-separator v-if="!newUser" />
<q-card-section v-if="!newUser" class="fit row justify-start content-center items-center">
<PasswordInput
v-if="isCurrentUser"
v-model="password"
:rules="[notEmpty]"
filled
label="Passwort"
autocomplete="current-password"
class="col-xs-12 col-sm-6 q-pa-sm"
hint="Passwort muss immer eingetragen werden"
/>
<PasswordInput
v-model="newPassword"
filled
label="Neues Password"
autocomplete="new-password"
class="col-xs-12 col-sm-6 q-pa-sm"
/>
</q-card-section>
<q-card-actions align="right">
<q-btn label="Reset" type="reset" />
<q-btn color="primary" type="submit" label="Speichern" />
</q-card-actions>
</q-form>
</template>
<script lang="ts">
import { Notify } from 'quasar';
import { IsoDateInput, PasswordInput } from '@flaschengeist/api/components';
import { defineComponent, computed, ref, onBeforeMount, PropType, watchEffect } from 'vue';
import { hasPermission, notEmpty, isEmail, useMainStore, useUserStore } from '@flaschengeist/api';
export default defineComponent({
name: 'MainUserSettings',
components: { IsoDateInput, PasswordInput },
props: {
user: {
required: true,
type: Object as PropType<FG.User>,
},
newUser: { type: Boolean, default: false },
},
emits: {
'update:user': (payload: FG.User) => !!payload,
},
setup(props, { emit }) {
const userStore = useUserStore();
const mainStore = useMainStore();
onBeforeMount(() => {
void userStore.getRoles(false);
});
const password = ref('');
const newPassword = ref('');
const avatar = ref<File | FileList | string[]>();
const userModel = ref(Object.assign({}, props.user));
const canSetRoles = computed(() => hasPermission('users_set_roles'));
const allRoles = computed(() => userStore.roles.map((role) => role.name));
const isCurrentUser = computed(() => userModel.value.userid === mainStore.currentUser.userid);
/* Reset model if props changed */
watchEffect(() => {if(props.user.userid && props.user.userid !== userModel.value.userid) reset()})
function onAvatarRejected() {
Notify.create({
group: false,
type: 'negative',
message: 'Datei zu groß oder keine gültige Bilddatei.',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
avatar.value = undefined;
}
function save() {
let changed = userModel.value;
if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday);
changed = Object.assign(changed, {
password: password.value,
});
if (newPassword.value != '') {
changed = Object.assign(changed, {
new_password: newPassword.value,
});
}
emit('update:user', changed);
if (avatar.value)
userStore
.uploadAvatar(changed, avatar.value instanceof File ? avatar.value : avatar.value[0])
.catch((response: Response) => {
if (response && response.status == 400) {
onAvatarRejected();
}
});
}
function reset() {
userModel.value = Object.assign({}, props.user);
password.value = '';
newPassword.value = '';
}
function isFreeUID(val: string) {
return (
userStore.users.findIndex((user) => user.userid === val) === -1 ||
'Benutzername ist schon vergeben'
);
}
return {
allRoles,
avatar,
canSetRoles,
isCurrentUser,
isEmail,
isFreeUID,
newPassword,
notEmpty,
onAvatarRejected,
password,
reset,
save,
userModel,
};
},
});
</script>

View File

@ -0,0 +1,160 @@
<template>
<div>
<q-card class="col-12">
<q-form @submit="save" @reset="reset">
<q-card-section class="fit row justify-start content-center items-center">
<span class="col-xs-12 col-sm-6 text-center text-h6"> Rollen und Berechtigungen </span>
<q-select
filled
use-input
label="Rolle"
input-debounce="0"
class="col-xs-12 col-sm-6 q-pa-sm"
:model-value="role"
:options="roles"
option-label="name"
option-value="name"
map-options
clearable
@new-value="createRole"
@update:modelValue="updateRole"
@clear="removeRole"
/>
</q-card-section>
<q-separator />
<q-card-section v-if="role">
<q-input v-if="role.id !== -1" v-model="newRoleName" filled label="neuer Name" />
<q-scroll-area style="height: 40vh; width: 100%" class="background-like-input">
<q-option-group
:model-value="role.permissions"
:options="permissions"
color="primary"
type="checkbox"
@update:modelValue="updatePermissions"
/>
</q-scroll-area>
</q-card-section>
<q-card-actions v-if="role" align="right">
<q-btn label="Löschen" color="negative" @click="remove" />
<q-btn label="Reset" type="reset" />
<q-btn color="primary" type="submit" label="Speichern" />
</q-card-actions>
</q-form>
</q-card>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, onBeforeMount } from 'vue';
import { useUserStore } from '@flaschengeist/api';
export default defineComponent({
name: 'RoleSettings',
setup() {
const userStore = useUserStore();
onBeforeMount(() => {
void userStore.getRoles();
void userStore.getPermissions();
});
const role = ref<FG.Role | null>(null);
const roles = computed(() => userStore.roles);
const permissions = computed(() =>
userStore.permissions.map((perm) => {
return {
value: perm,
label: perm,
};
})
);
const newRoleName = ref<string>('');
function createRole(name: string, done: (arg0: string, arg1: string) => void): void {
role.value = { name: name, permissions: [], id: -1 };
done(name, 'add-unique');
}
function removeRole(): void {
role.value = null;
}
function updatePermissions(permissions: string[]) {
if (role.value) {
role.value.permissions = permissions;
}
}
function updateRole(rl: FG.Role | string | null) {
if (typeof rl === 'string' || rl === null) return;
role.value = {
id: rl.id,
name: rl.name,
permissions: Array.from(rl.permissions),
};
}
function save() {
if (role.value) {
if (role.value.id === -1)
void userStore.newRole(role.value).then((createdRole: FG.Role) => {
role.value = createdRole;
});
else {
if (newRoleName.value !== '') role.value.name = newRoleName.value;
void userStore.updateRole(role.value);
}
}
}
function reset() {
if (role.value && role.value.id !== -1) {
const original = roles.value.find((value) => value.name === role.value?.name);
if (original) updateRole(original);
} else {
role.value = null;
}
}
function remove() {
if (role.value) {
if (role.value.id === -1) {
role.value = null;
} else {
void userStore.deleteRole(role.value).then(() => (role.value = null));
}
}
}
return {
roles,
role,
permissions,
createRole,
updateRole,
updatePermissions,
save,
reset,
removeRole,
remove,
newRoleName,
};
},
});
</script>
<style lang="sass" scoped>
// Same colors like qinput with filled attribute set
.body--light .background-like-input
background-color: rgba(0, 0, 0, 0.05)
&:hover
background: rgba(0,0,0,.1)
border-bottom: 1px solid rgba(0, 0, 0, 0.42)
.body--dark .background-like-input
background-color: rgba(255, 255, 255, 0.07)
&:hover
background: rgba(255, 255, 255, 0.14)
border-bottom: 1px solid #fff
</style>

View File

@ -0,0 +1,166 @@
<template>
<q-card class="col-12" height="">
<q-card-section v-if="isThisSession(modelValue.token)" class="text-caption">
Diese Session.
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-xs-12 col-sm-6">
Browser:
<q-icon :name="getBrowserIcon(modelValue.browser)" size="24px" />
{{ modelValue.browser }}
</div>
<div class="col-xs-12 col-sm-6">
Plattform:
<q-icon :name="getPlatformIcon(modelValue.platform)" size="24px" />
{{ modelValue.platform }}
</div>
</div>
<div v-if="!isEdit" class="row">
<div class="col-xs-12 col-sm-6">
Lebenszeit:
{{ modelValue.lifetime }}
</div>
<div class="col-xs-12 col-sm-6">Läuft aus: {{ dateTime(modelValue.expires) }}</div>
</div>
<div v-else class="row q-my-sm">
<q-input
v-model="computedLifetime"
class="col-xs-12 col-sm-6 q-px-sm"
type="number"
label="Zeit"
filled
/>
<q-select v-model="option" class="col-xs-12 col-sm-6 q-px-sm" :options="options" filled />
</div>
</q-card-section>
<q-card-actions v-if="!isEdit" align="right">
<q-btn flat round dense icon="mdi-pencil" @click="edit(true)" />
<q-btn flat round dense icon="mdi-delete" @click="deleteSession(modelValue.token)" />
</q-card-actions>
<q-card-actions v-else align="right">
<q-btn flat dense label="Abbrechen" @click="edit(false)" />
<q-btn flat dense label="Speichern" @click="save" />
</q-card-actions>
</q-card>
</template>
<script lang="ts">
import { defineComponent, ref, computed, PropType } from 'vue';
import { formatDateTime, useMainStore, useSessionStore } from '@flaschengeist/api';
export default defineComponent({
name: 'Session',
props: {
modelValue: {
required: true,
type: Object as PropType<FG.Session>,
},
},
emits: {
'update:modelValue': (s: FG.Session) => !!s,
delete: () => true,
},
setup(props, { emit }) {
const sessionStore = useSessionStore();
const mainStore = useMainStore();
const dateTime = (date: Date) => formatDateTime(date, true);
const options = ref(['Minuten', 'Stunden', 'Tage']);
const option = ref<string>(options.value[0]);
const lifetime = ref(0);
function getBrowserIcon(browser: string) {
return browser == 'firefox'
? 'mdi-firefox'
: browser == 'chrome'
? 'mdi-google-chrome'
: browser == 'safari'
? 'mdi-apple-safari'
: 'mdi-help';
}
function getPlatformIcon(platform: string) {
return platform == 'linux'
? 'mdi-linux'
: platform == 'windows'
? 'mdi-microsoft-windows'
: platform == 'macos'
? 'mdi-apple'
: platform == 'iphone'
? 'mdi-cellphone-iphone'
: platform == 'android'
? 'mdi-cellphone-android'
: 'mdi-help';
}
async function deleteSession(token: string) {
await sessionStore.deleteSession(token);
emit('delete');
}
function isThisSession(token: string) {
return mainStore.session?.token === token;
}
const isEdit = ref(false);
const computedLifetime = computed({
get: () => {
switch (option.value) {
case options.value[0]:
return (lifetime.value / 60).toFixed(2);
case options.value[1]:
return (lifetime.value / (60 * 60)).toFixed(2);
case options.value[2]:
return (lifetime.value / (60 * 60 * 24)).toFixed(2);
}
throw 'Invalid option';
},
set: (val) => {
if (val) {
switch (option.value) {
case options.value[0]:
lifetime.value = parseFloat(val) * 60;
break;
case options.value[1]:
lifetime.value = parseFloat(val) * 60 * 60;
break;
case options.value[2]:
lifetime.value = parseFloat(val) * 60 * 60 * 24;
break;
}
}
},
});
function edit(value: boolean) {
lifetime.value = props.modelValue.lifetime;
isEdit.value = value;
}
async function save() {
isEdit.value = false;
await sessionStore.updateSession(lifetime.value, props.modelValue.token);
emit(
'update:modelValue',
Object.assign(Object.assign({}, props.modelValue), { lifetime: lifetime.value })
);
}
return {
getBrowserIcon,
getPlatformIcon,
isThisSession,
deleteSession,
isEdit,
dateTime,
edit,
options,
option,
lifetime,
computedLifetime,
save,
};
},
});
</script>

21
src/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { FG_Plugin } from '@flaschengeist/types';
import { defineAsyncComponent } from 'vue';
import routes from './routes';
const plugin: FG_Plugin.Plugin = {
id: 'users',
name: 'User',
innerRoutes: routes,
requiredModules: [['auth'], ['users'], ['roles']],
version: '0.0.1',
widgets: [
{
priority: 1,
name: 'greeting',
permissions: [],
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
},
],
};
export default plugin

14
src/models.ts Normal file
View File

@ -0,0 +1,14 @@
export interface LoginData {
userid: string;
password: string;
}
export interface LoginResponse {
user: FG.User;
session: FG.Session;
permissions: FG.Permission[];
}
export interface CurrentUserResponse extends FG.User {
permissions: FG.Permission[];
}

View File

@ -0,0 +1,93 @@
<template>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab
v-for="(tabindex, index) in tabs"
:key="'tab' + index"
:name="tabindex.name"
:label="tabindex.label"
/>
</q-tabs>
<div v-else class="fit row justify-end">
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
</div>
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
<q-list v-model="tab">
<q-item
v-for="(tabindex, index) in tabs"
:key="'tab' + index"
:active="tab == tabindex.name"
clickable
@click="tab = tabindex.name"
>
<q-item-label>{{ tabindex.label }}</q-item-label>
</q-item>
</q-list>
</q-drawer>
<q-tab-panels
v-model="tab"
style="background-color: transparent"
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
animated
>
<q-tab-panel name="user">
<UpdateUser />
</q-tab-panel>
<q-tab-panel name="newUser">
<NewUser />
</q-tab-panel>
<q-tab-panel name="roles">
<RoleSettings v-if="canEditRoles" />
</q-tab-panel>
</q-tab-panels>
</q-page>
</template>
<script lang="ts">
import { Screen } from 'quasar';
import { PERMISSIONS } from '../permissions';
import NewUser from '../components/NewUser.vue';
import { hasPermission } from '@flaschengeist/api';
import { computed, defineComponent, ref } from 'vue';
import UpdateUser from '../components/UpdateUser.vue';
import RoleSettings from '../components/settings/RoleSettings.vue';
export default defineComponent({
name: 'AdminSettings',
components: { RoleSettings, UpdateUser, NewUser },
setup() {
const canEditRoles = computed(() => hasPermission(PERMISSIONS.ROLES_EDIT));
interface Tab {
name: string;
label: string;
}
const tabs: Tab[] = [
{ name: 'user', label: 'Mitglieder' },
{ name: 'newUser', label: 'Neues Mitglied' },
{ name: 'roles', label: 'Rollen' },
];
const drawer = ref<boolean>(false);
const showDrawer = computed({
get: () => {
return !Screen.gt.sm && drawer.value;
},
set: (val: boolean) => {
drawer.value = val;
},
});
const tab = ref<string>('user');
return {
canEditRoles,
showDrawer,
tab,
tabs,
};
},
});
</script>

55
src/pages/Settings.vue Normal file
View File

@ -0,0 +1,55 @@
<template>
<div>
<q-page padding class="fit row justify-center content-center items-center q-gutter-sm">
<q-card class="col-12">
<q-card-section class="fit row justify-start content-center items-center">
<div class="col-12 text-center text-h6">Benutzereinstellungen</div>
</q-card-section>
<MainUserSettings :user="currentUser" @update:user="updateUser" />
</q-card>
<div class="col-12 text-left text-h6">Aktive Sessions:</div>
<Session
v-for="(session, index) in sessions"
:key="'session' + index"
v-model="sessions[index]"
@delete="removeSession(session)"
/>
</q-page>
</div>
</template>
<script lang="ts">
import { useMainStore, useUserStore, useSessionStore } from '@flaschengeist/api';
import MainUserSettings from '../components/settings/MainUserSettings.vue';
import { defineComponent, onBeforeMount, ref } from 'vue';
import Session from '../components/settings/Session.vue';
export default defineComponent({
// name: 'PageName'
components: { Session, MainUserSettings },
setup() {
const mainStore = useMainStore();
const sessionStore = useSessionStore();
const userStore = useUserStore();
onBeforeMount(() => sessionStore.getSessions().then((s) => (sessions.value = s)));
const currentUser = ref(mainStore.currentUser);
const sessions = ref([] as FG.Session[]);
async function updateUser(value: FG.User) {
await userStore.updateUser(value);
}
function removeSession(s: FG.Session) {
sessions.value = sessions.value.filter((ss) => ss.token !== s.token);
}
return {
currentUser,
sessions,
updateUser,
removeSession,
};
},
});
</script>

12
src/permissions.ts Normal file
View File

@ -0,0 +1,12 @@
export const PERMISSIONS = {
// Kann andere Nutzer bearbeiten
EDIT_OTHER: 'users_edit_other',
// Kann Rollen von Nutzern setzen
SET_ROLES: 'users_set_roles',
// Kann Nutzer löschen
DELETE: 'users_delete_other',
// Kann neue Nutzer hinzufügen
REGISTER: 'users_register',
// Kann Rollen löschen oder bearbeiten, z.b. Rechte hinzufügen etc
ROLES_EDIT: 'roles_edit',
};

39
src/routes/index.ts Normal file
View File

@ -0,0 +1,39 @@
import { pinia, useMainStore } from '@flaschengeist/api';
import { FG_Plugin } from '@flaschengeist/types';
const mainRoutes: FG_Plugin.MenuRoute[] = [
{
get title() {
return () => useMainStore(pinia.value).currentUser.display_name;
},
icon: 'mdi-account',
permissions: ['user'],
route: { path: 'user', name: 'user', redirect: { name: 'user-settings' } },
children: [
{
title: 'Einstellungen',
icon: 'mdi-account-edit',
shortcut: true,
permissions: ['user'],
route: {
path: 'settings',
name: 'user-settings',
component: () => import('../pages/Settings.vue'),
},
},
{
title: 'Admin',
icon: 'mdi-cog',
shortcut: false,
permissions: ['users_edit_other'],
route: {
path: 'admin',
name: 'admin-settings',
component: () => import('../pages/AdminSettings.vue'),
},
},
],
},
];
export default mainRoutes;

4
src/shims.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@quasar/app/tsconfig-preset",
"target": "esnext",
"compilerOptions": {
"baseUrl": "src/",
"lib": [
"es2020",
"dom"
],
"types": [
"@flaschengeist/types",
"@quasar/app",
"node"
]
}
}