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