Transfered files from flaschengeist main tree
This commit is contained in:
parent
a108cbdbbd
commit
5a5bbd0dbd
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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[];
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.vue' {
|
||||
import Vue from 'vue';
|
||||
export default Vue;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@quasar/app/tsconfig-preset",
|
||||
"target": "esnext",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src/",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"@flaschengeist/types",
|
||||
"@quasar/app",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue