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