@@ -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') |
@@ -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 } | |||
} |
@@ -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') | |||
} | |||
} | |||
} |
@@ -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') | |||
} | |||
] | |||
} | |||
] | |||
@@ -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 | |||
} | |||
}) |
@@ -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> |
@@ -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) { |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -7,6 +7,7 @@ type Person { | |||
givenName: String! | |||
familyName: String! | |||
email: EmailAddress! | |||
master: Boolean | |||
token: String | |||
_adminOf: [UUID!] | |||
adminOf: [Organizer!] |
@@ -15,4 +15,7 @@ export class Person { | |||
@Field(() => EmailAddress , { nullable: false }) | |||
email: EmailAddress | |||
@Field(() => Boolean, { nullable: true }) | |||
master?: boolean | |||
} |
@@ -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, |