瀏覽代碼

Ckient Login, Admin-Funktionen initialisiert

tags/v0.9.1
akimmig 4 年之前
父節點
當前提交
22bb9cd609
共有 14 個檔案被更改,包括 577 行新增84 行删除
  1. +2
    -0
      client/src/main.js
  2. +9
    -0
      client/src/plugins/auth.js
  3. +5
    -1
      client/src/plugins/graphql.js
  4. +23
    -1
      client/src/plugins/router.js
  5. +2
    -1
      client/src/plugins/store.js
  6. +161
    -0
      client/src/views/Login.vue
  7. +2
    -46
      client/src/views/components/core/AppBar.vue
  8. +10
    -35
      client/src/views/components/core/Menu.vue
  9. +115
    -0
      client/src/views/components/management/apparatus.vue
  10. +120
    -0
      client/src/views/components/management/organizer.vue
  11. +115
    -0
      client/src/views/components/management/person.vue
  12. +1
    -0
      server/schema.gql
  13. +3
    -0
      server/src/person/models/Person.ts
  14. +9
    -0
      server/src/person/resolver/person.ts

+ 2
- 0
client/src/main.js 查看文件

@@ -3,6 +3,7 @@ import App from './App.vue'
import router from './plugins/router'
import store from './plugins/store'
import vuetify from './plugins/vuetify'
import apolloProvider from './plugins/graphql'

import '@mdi/font/css/materialdesignicons.css'
import '@fortawesome/fontawesome-pro/css/all.css'
@@ -22,5 +23,6 @@ new Vue({
router,
store,
vuetify,
apolloProvider,
render: h => h(App)
}).$mount('#app')

+ 9
- 0
client/src/plugins/auth.js 查看文件

@@ -0,0 +1,9 @@
import { computed } from '@vue/composition-api'

export const useAuth = (context) => {
const isMaster = computed(() => {
return context.root.$store.getters.isMaster
})

return { isMaster }
}

+ 5
- 1
client/src/plugins/graphql.js 查看文件

@@ -1,3 +1,4 @@
import Vue from 'vue'
import { v4 as uuid } from 'uuid'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import ApolloClient from 'apollo-client'
@@ -7,6 +8,8 @@ import * as Cookie from 'js-cookie'
import { ref } from '@vue/composition-api'
import gql from 'graphql-tag'

Vue.use(VueApollo)

export const clientId = uuid()

const server = process.env.NODE_ENV === 'production' ? 'wss://turnenaufzeit.de/gql' : 'ws://localhost:3000/graphql'
@@ -62,6 +65,7 @@ export const useGraphQL = (context) => {
givenName
familyName
adminOf { _id name plz ort }
master
}}`,
variables: {
token: !email || !passwort ? Cookie.get('token') : undefined,
@@ -79,7 +83,7 @@ export const useGraphQL = (context) => {
Cookie.remove('token')

if (email) {
context.root.$store.commit('OPEN_SNACKBAR', 'Zugangsdaten falsch')
// context.root.$store.commit('OPEN_SNACKBAR', 'Zugangsdaten falsch')
}
}
}

+ 23
- 1
client/src/plugins/router.js 查看文件

@@ -7,7 +7,29 @@ const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Index')
component: () => import('../views/Index'),
children: [
{
path: 'login',
name: 'Login',
component: () => import('../views/Login')
},
{
path: 'management/person',
name: 'Personen bearbeiten',
component: () => import('../views/components/management/person')
},
{
path: 'management/apparatus',
name: 'Geräte bearbeiten',
component: () => import('../views/components/management/apparatus')
},
{
path: 'management/organizer',
name: 'Veranstalter bearbeiten',
component: () => import('../views/components/management/organizer')
}
]
}
]


+ 2
- 1
client/src/plugins/store.js 查看文件

@@ -34,6 +34,7 @@ export default new Vuex.Store({
},
getters: {
profile: (state) => state.profile || {},
isMaster: (state) => !!state.profile?.master
isMaster: (state) => !!state.profile?.master,
isLogin: (state) => !!state.profile?.token
}
})

+ 161
- 0
client/src/views/Login.vue 查看文件

@@ -0,0 +1,161 @@
<template>
<v-container
id="login"
class="fill-height justify-center"
tag="section"
>
<v-row justify="center">
<base-material-card
color="red"
light
max-width="100%"
width="400"
class="px-5 py-3"
>
<template #heading>
<div class="text-center">
<h1 class="display-2 font-weight-bold mb-2">
Anmelden
</h1>
</div>
</template>

<v-card-text class="text-center">
<v-form
ref="login"
v-model="valid"
lazy-validation
>
<v-text-field
v-model="email"
required
:rules="emailRules"
color="red"
label="E-Mail..."
prepend-icon="mdi-email"
name="email"
@keydown.enter="_login"
/>

<v-text-field
v-model="password"
required
:rules="passwordRules"
:type="pwdshow ? 'text' : 'password'"
class="mb-5"
color="red"
label="Passwort..."
prepend-icon="mdi-lock"
name="password"
:append-icon="pwdshow ? 'mdi-eye' : 'mdi-eye-off'"
@click:append="pwdshow = !pwdshow"
@keydown.enter="_login"
/>

<v-btn
class="mb-5"
color="red"
tile
@click="_login"
width="100%"
>
<v-icon class="mr-4">
fa-sign-in-alt
</v-icon>
Anmelden
</v-btn>
<v-btn
class="mb-5"
color="white"
tile
to="/register"
width="100%"
>
<v-icon class="mr-4">
far fa-user-plus
</v-icon>
Neu registrieren
</v-btn>
<v-btn
class="mb-5"
color="white"
tile
to="/reset"
width="100%"
>
<v-icon class="mr-4">
far fa-key
</v-icon>
Passwort vergessen
</v-btn>
</v-form>
</v-card-text>
</base-material-card>
</v-row>
</v-container>
</template>

<script>
import { mapGetters } from 'vuex'
import { useGraphQL } from '@/plugins/graphql'

export default {
name: 'PagesLogin',

setup (props, context) {
return {
...useGraphQL(context)
}
},

data: () => ({
valid: true,
email: '',
password: '',
emailRules: [
v => !!v || 'E-Mail wird benötigt!'
],
passwordRules: [
v => !!v || 'Passwort wird benötigt!'
],
pwdshow: false
}),

computed: {
...mapGetters(['profile', 'isLogin'])
},

watch: {
isLogin () {
if (this.isLogin) {
this.cl()
}
}
},

created () {
if (this.isLogin) {
this.cl()
}
},

methods: {
async _login () {
if (!this.$refs.login.validate()) {
return
}

await this.login(this.email, this.password)

if (!this.isLogin) {
this.$store.commit('OPEN_SNACKBAR', 'Falsche Zugangsdaten!')
}
},
cl () {
let target = this.$route.query.origin
if (!target) target = '/'
this.$router.replace({ path: target })
}
}
}
</script>

+ 2
- 46
client/src/views/components/core/AppBar.vue 查看文件

@@ -39,56 +39,12 @@
<v-icon>mdi-view-dashboard</v-icon>
</v-btn>

<v-menu
bottom
left
offset-y
origin="top right"
transition="scale-transition"
>
<template #activator="{ attrs, on }">
<v-btn
class="ml-2"
min-width="0"
text
v-bind="attrs"
v-on="on"
>
<v-badge
color="rgb(255, 4, 29)"
overlap
bordered
>
<template #badge>
<span>{{ messages.length }}</span>
</template>

<v-icon>mdi-bell</v-icon>
</v-badge>
</v-btn>
</template>

<v-list
:tile="false"
nav
>
<div>
<app-bar-item
v-for="(n, i) in messages"
:key="`item-${i}`"
>
<v-list-item-title v-text="n.message" />
</app-bar-item>
</div>
</v-list>
</v-menu>

<v-btn
v-if="!profile._id"
class="ml-2"
min-width="0"
text
to="/pages/login"
to="/login"
>
<v-icon>far fa-user</v-icon>
</v-btn>
@@ -162,7 +118,7 @@ export default {
},

computed: {
...mapState(['drawer', 'messages']),
...mapState(['drawer']),
...mapGetters(['profile']),
title () {
if (this.$route.name) {

+ 10
- 35
client/src/views/components/core/Menu.vue 查看文件

@@ -12,49 +12,24 @@
:item="{
group: '/management',
icon: 'fa-user-crown',
title: 'Admin',
title: 'Administration',
children: [
{
title: 'Wettkampforte',
to: 'places',
icon: 'mdi-home-group',
},
{
title: 'Hauptevents',
to: 'events',
icon: 'mdi-calendar-multiple',
title: 'Geräte',
to: 'apparatus',
icon: 'fa-dumbbell',
},
{
title: 'Vereine',
to: 'clubs',
title: 'Veranstalter',
to: 'organizer',
icon: 'mdi-account-supervisor-circle',
},
{
title: 'Disziplinen',
to: 'disciplines',
icon: 'fa-dumbbell',
},
{
title: 'Personen verwalten',
to: 'people',
title: 'Personen',
to: 'person',
icon: 'mdi-account-edit',
},
{
title: 'Personen zusammenführen',
to: 'merge',
icon: 'mdi-account-switch',
},
{
title: 'Turnportalabfrage',
to: 'turnportal',
icon: 'mdi-account-question',
},
{
title: 'Serverübersicht',
to: 'server',
icon: 'mdi-server',
},
],
}
]
}"
/>
</div>

+ 115
- 0
client/src/views/components/management/apparatus.vue 查看文件

@@ -0,0 +1,115 @@
<template>
<e403 v-if="!isMaster" />
<v-container
v-else
fluid
tag="section"
>
<v-card
flat
>
<v-btn
absolute
top
right
fab
small
@click="open(null)"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>

<v-col>
<v-text-field
v-model="filter"
label="Filter"
/>
</v-col>

<v-data-table
:headers="headers"
:items="ApparatusFind"
sort-by="name"
:items-per-page="15"
mobile-breakpoint="0"
:search="filter"
@click:row="open"
/>
</v-card>
<!--edit-people
:id="dialog.id"
v-model="dialog.open"
/-->
</v-container>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import gql from 'graphql-tag'
// import { updateQuery, deleteQuery } from '@/graphql'

export default {
name: 'People',

components: {
// EditPeople: () => import('./dialogs/EditPeople'),
},

setup (props, context) {
return {
...useAuth(context)
}
},

data: () => ({
ApparatusFind: [],
headers: [
{
text: '',
value: 'logo',
sortable: false
},
{
text: 'Name',
value: 'name',
sortable: true
}
],
dialog: {
open: false,
id: null
},
filter: ''
}),

apollo: {
ApparatusFind: {
query: gql`query { ApparatusFind { _id name logo } }`
/* subscribeToMore: {
document: gql`subscription { PersonUpdated { _id vorname nachname geburtstag dtbid gymnet { id } email { email typ } tel { nummer typ } adresse { strasse hausnummer plz ort land typ } turnportal { typ gueltigvon gueltigbis lastcheck sperre festgeturnt verein vermerk } } }`,
updateQuery: updateQuery('PersonList', 'PersonUpdated')
} */
}
/* $subscribe: {
PersonDeleted: {
query: gql`subscription { PersonDeleted }`,
result (id) {
deleteQuery('PersonList', 'PersonUpdated', this.PersonList, id)
},
},
}, */
},

methods: {
open (item) {
this.dialog.open = true
this.dialog.id = item?._id
}
}
}
</script>

<style scoped>
</style>

+ 120
- 0
client/src/views/components/management/organizer.vue 查看文件

@@ -0,0 +1,120 @@
<template>
<e403 v-if="!isMaster" />
<v-container
v-else
fluid
tag="section"
>
<v-card
flat
>
<v-btn
absolute
top
right
fab
small
@click="open(null)"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>

<v-col>
<v-text-field
v-model="filter"
label="Filter"
/>
</v-col>

<v-data-table
:headers="headers"
:items="OrganizerFind"
sort-by="plz"
:items-per-page="15"
mobile-breakpoint="0"
:search="filter"
@click:row="open"
/>
</v-card>
<!--edit-people
:id="dialog.id"
v-model="dialog.open"
/-->
</v-container>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import gql from 'graphql-tag'
// import { updateQuery, deleteQuery } from '@/graphql'

export default {
name: 'People',

components: {
// EditPeople: () => import('./dialogs/EditPeople'),
},

setup (props, context) {
return {
...useAuth(context)
}
},

data: () => ({
OrganizerFind: [],
headers: [
{
text: 'Name',
value: 'name',
sortable: true
},
{
text: 'PLZ',
value: 'plz',
sortable: true
},
{
text: 'Ort',
value: 'ort',
sortable: true
}
],
dialog: {
open: false,
id: null
},
filter: ''
}),

apollo: {
OrganizerFind: {
query: gql`query { OrganizerFind { _id name plz ort } }`
/* subscribeToMore: {
document: gql`subscription { PersonUpdated { _id vorname nachname geburtstag dtbid gymnet { id } email { email typ } tel { nummer typ } adresse { strasse hausnummer plz ort land typ } turnportal { typ gueltigvon gueltigbis lastcheck sperre festgeturnt verein vermerk } } }`,
updateQuery: updateQuery('PersonList', 'PersonUpdated')
} */
}
/* $subscribe: {
PersonDeleted: {
query: gql`subscription { PersonDeleted }`,
result (id) {
deleteQuery('PersonList', 'PersonUpdated', this.PersonList, id)
},
},
}, */
},

methods: {
open (item) {
this.dialog.open = true
this.dialog.id = item?._id
}
}
}
</script>

<style scoped>
</style>

+ 115
- 0
client/src/views/components/management/person.vue 查看文件

@@ -0,0 +1,115 @@
<template>
<e403 v-if="!isMaster" />
<v-container
v-else
fluid
tag="section"
>
<v-card
flat
>
<v-btn
absolute
top
right
fab
small
@click="open(null)"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>

<v-col>
<v-text-field
v-model="filter"
label="Filter"
/>
</v-col>

<v-data-table
:headers="headers"
:items="PersonFind"
sort-by="familyName"
:items-per-page="15"
mobile-breakpoint="0"
:search="filter"
@click:row="open"
/>
</v-card>
<!--edit-people
:id="dialog.id"
v-model="dialog.open"
/-->
</v-container>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import gql from 'graphql-tag'
// import { updateQuery, deleteQuery } from '@/graphql'

export default {
name: 'People',

components: {
// EditPeople: () => import('./dialogs/EditPeople'),
},

setup (props, context) {
return {
...useAuth(context)
}
},

data: () => ({
PersonFind: [],
headers: [
{
text: 'Nachname',
value: 'familyName',
sortable: true
},
{
text: 'Vorname',
value: 'givenName',
sortable: true
}
],
dialog: {
open: false,
id: null
},
filter: ''
}),

apollo: {
PersonFind: {
query: gql`query { PersonFind { _id familyName givenName } }`
/* subscribeToMore: {
document: gql`subscription { PersonUpdated { _id vorname nachname geburtstag dtbid gymnet { id } email { email typ } tel { nummer typ } adresse { strasse hausnummer plz ort land typ } turnportal { typ gueltigvon gueltigbis lastcheck sperre festgeturnt verein vermerk } } }`,
updateQuery: updateQuery('PersonList', 'PersonUpdated')
} */
}
/* $subscribe: {
PersonDeleted: {
query: gql`subscription { PersonDeleted }`,
result (id) {
deleteQuery('PersonList', 'PersonUpdated', this.PersonList, id)
},
},
}, */
},

methods: {
open (item) {
this.dialog.open = true
this.dialog.id = item?._id
}
}
}
</script>

<style scoped>
</style>

+ 1
- 0
server/schema.gql 查看文件

@@ -7,6 +7,7 @@ type Person {
givenName: String!
familyName: String!
email: EmailAddress!
master: Boolean
token: String
_adminOf: [UUID!]
adminOf: [Organizer!]

+ 3
- 0
server/src/person/models/Person.ts 查看文件

@@ -15,4 +15,7 @@ export class Person {

@Field(() => EmailAddress , { nullable: false })
email: EmailAddress

@Field(() => Boolean, { nullable: true })
master?: boolean
}

+ 9
- 0
server/src/person/resolver/person.ts 查看文件

@@ -50,6 +50,15 @@ export class PersonResolver {
return parent.email;
}

@ResolveField(() => Boolean, { nullable: true })
async master(
@Context('client') client: Client,
@Parent() parent: Person
): Promise<boolean> {
if (!client.isMaster() && !client.isSelf(parent._id)) return null;
return parent.master;
}

@ResolveField(() => [UUID], { nullable: true })
async _adminOf(
@Context('client') client: Client,

Loading…
取消
儲存