Просмотр исходного кода

Registrierung, Passwort vergessen, Urkunden, Startseite

tags/v0.9.1
akimmig 4 лет назад
Родитель
Сommit
20d31593f3
24 измененных файлов: 1242 добавлений и 24 удалений
  1. +2
    -0
      client/.gitignore
  2. +74
    -4
      client/package-lock.json
  3. +2
    -0
      client/package.json
  4. +6
    -1
      client/src/App.vue
  5. Двоичные данные
      client/src/assets/bogen.png
  6. Двоичные данные
      client/src/assets/logo_sim.jpg
  7. +22
    -0
      client/src/plugins/router.js
  8. +105
    -0
      client/src/urkunde.js
  9. +111
    -0
      client/src/views/Confirm.vue
  10. +10
    -1
      client/src/views/Index.vue
  11. +241
    -0
      client/src/views/Register.vue
  12. +208
    -0
      client/src/views/Reset.vue
  13. +3
    -2
      client/src/views/components/core/Drawer.vue
  14. +109
    -1
      client/src/views/components/event/event.vue
  15. +41
    -0
      client/src/views/components/start.vue
  16. +1
    -0
      server/.gitignore
  17. +133
    -0
      server/package-lock.json
  18. +2
    -0
      server/package.json
  19. +1
    -0
      server/schema.gql
  20. +54
    -0
      server/src/config.ts
  21. +5
    -12
      server/src/db.ts
  22. +40
    -0
      server/src/mail.ts
  23. +3
    -1
      server/src/main.ts
  24. +69
    -2
      server/src/person/resolver/person.mutation.ts

+ 2
- 0
client/.gitignore Просмотреть файл

@@ -21,3 +21,5 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?

/src/env.js

+ 74
- 4
client/package-lock.json Просмотреть файл

@@ -9,6 +9,7 @@
"dependencies": {
"@fortawesome/fontawesome-pro": "^5.15.3",
"@mdi/font": "^5.9.55",
"@pdf-lib/fontkit": "^1.1.1",
"@vue/composition-api": "1.0.0-rc.12",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
@@ -17,6 +18,7 @@
"js-cookie": "^2.2.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"pdf-lib": "^1.16.0",
"subscriptions-transport-ws": "^0.9.19",
"uuid": "^3.4.0",
"vue": "^2.6.14",
@@ -1473,6 +1475,30 @@
"node": ">= 6"
}
},
"node_modules/@pdf-lib/fontkit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
"integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -10634,8 +10660,7 @@
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parallel-transform": {
"version": "1.2.0",
@@ -10837,6 +10862,17 @@
"node": ">=0.12"
}
},
"node_modules/pdf-lib": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.16.0.tgz",
"integrity": "sha512-P/1SSmElOBKrPlbc+Sn7UxikRQbzVA64+4Dh6/uczPscvq/NatP9eryoOguyBTpTnzICNiG8EnMH4Ziqp2TnFA==",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -17585,6 +17621,30 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
"@pdf-lib/fontkit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
"integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==",
"requires": {
"pako": "^1.0.6"
}
},
"@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"requires": {
"pako": "^1.0.6"
}
},
"@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"requires": {
"pako": "^1.0.10"
}
},
"@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -24932,8 +24992,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parallel-transform": {
"version": "1.2.0",
@@ -25108,6 +25167,17 @@
"sha.js": "^2.4.8"
}
},
"pdf-lib": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.16.0.tgz",
"integrity": "sha512-P/1SSmElOBKrPlbc+Sn7UxikRQbzVA64+4Dh6/uczPscvq/NatP9eryoOguyBTpTnzICNiG8EnMH4Ziqp2TnFA==",
"requires": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",

+ 2
- 0
client/package.json Просмотреть файл

@@ -10,6 +10,7 @@
"dependencies": {
"@fortawesome/fontawesome-pro": "^5.15.3",
"@mdi/font": "^5.9.55",
"@pdf-lib/fontkit": "^1.1.1",
"@vue/composition-api": "1.0.0-rc.12",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
@@ -18,6 +19,7 @@
"js-cookie": "^2.2.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"pdf-lib": "^1.16.0",
"subscriptions-transport-ws": "^0.9.19",
"uuid": "^3.4.0",
"vue": "^2.6.14",

+ 6
- 1
client/src/App.vue Просмотреть файл

@@ -21,8 +21,13 @@
class="error_snack"
:timeout="5000"
top
multi-line
>
{{ error_text }}
<template
#default
>
<span v-html="error_text" />
</template>

<template #action="{ attrs }">
<v-btn

Двоичные данные
client/src/assets/bogen.png Просмотреть файл

Before After
Width: 853  |  Height: 679  |  Size: 95KB

Двоичные данные
client/src/assets/logo_sim.jpg Просмотреть файл

Before After
Width: 1661  |  Height: 1079  |  Size: 74KB

+ 22
- 0
client/src/plugins/router.js Просмотреть файл

@@ -10,11 +10,33 @@ const routes = [
component: () => import('../views/Index'),
children: [
{
path: '',
name: 'Startseite',
component: () => import('../views/components/start')
},
{
path: 'login',
name: 'Login',
component: () => import('../views/Login')
},
{
path: 'reset/:code?',
name: 'Passwort zurücksetzen',
component: () => import('../views/Reset'),
props: true
},
{
path: 'register',
name: 'Registrierung',
component: () => import('../views/Register')
},
{
path: 'confirm/:code?',
name: 'Bestätigung',
component: () => import('../views/Confirm'),
props: true
},
{
path: 'management/person',
name: 'Personen bearbeiten',
component: () => import('../views/components/management/person')

+ 105
- 0
client/src/urkunde.js
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 111
- 0
client/src/views/Confirm.vue Просмотреть файл

@@ -0,0 +1,111 @@
<template>
<v-container
fluid
tag="section"
>
<v-card
v-if="!code"
flat
>
<h1>Code fehlt. Bitte den Link aus der E-Mail nutzen!</h1>
</v-card>
<v-card
v-else-if="!confirmed"
flat
>
<h1>Bestätigung der Registrierung</h1>
<p>Bitte geben Sie zur Bestätigung nochmals Ihre E-Mail-Adresse ein.</p>
<v-row>
<v-col
cols="12"
>
<v-text-field
v-model="email"
label="E-Mail-Adresse"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="code"
label="Bestätigungscode"
readonly
/>
</v-col>
<v-col
cols="12"
>
<v-btn
block
@click="confirm"
>
Bestätigen
</v-btn>
</v-col>
</v-row>
</v-card>
<v-card
v-else
flat
>
<h1>Vielen Dank, Sie können sich nun anmelden.</h1>
</v-card>
</v-container>
</template>

<script>
import { useGraphQL } from '@/plugins/graphql'
import gql from 'graphql-tag'

export default {
name: 'PagesConfirm',

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

props: {
code: {
type: String,
required: true
}
},

data: () => ({
email: null,
confirmed: false
}),

methods: {
confirm () {
const err = []
const EMAIL_ADDRESS_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

if (!this.email) err.push('E-Mail-Adresse fehlt!')
if (this.email && !EMAIL_ADDRESS_REGEX.test(this.email)) err.push('E-Mail-Adresse ungültig!')

if (err.length !== 0) {
this.$store.commit('OPEN_SNACKBAR', `<ul><li>${err.join('</li><li>')}</li></ul>`)
return
}

this.$apollo.mutate({
mutation: gql`mutation($email: String!, $confirmCode: String!) {
PersonConfirmMail(email: $email, confirmCode: $confirmCode) { _id }
}`,
variables: {
email: this.email,
confirmCode: this.code
}
}).then(() => {
this.confirmed = true
}).catch(e => {
this.$store.commit('OPEN_SNACKBAR', e)
})
}
}
}
</script>

+ 10
- 1
client/src/views/Index.vue Просмотреть файл

@@ -16,6 +16,8 @@
</template>

<script>
import { generateUrkunde } from '@/urkunde'

export default {
name: 'DashboardIndex',

@@ -26,13 +28,20 @@ export default {
},

data: () => ({
expandOnHover: false
expandOnHover: false,
image: null
}),

computed: {
loading () {
return false // this.$store.state.loading
}
},

methods: {
async generatePDF () {
generateUrkunde([{ platz: '1. PLATZ', zeit: '0 MIN. 23 SEK.', team: 'Tolles Team' }, { platz: '2. PLATZ', zeit: '0 MIN. 24 SEK.', team: 'Fast Tolles Team' }, { platz: '3. PLATZ', zeit: '5 MIN. 24 SEK.', team: 'Schlechtes Team' }], 'Droste', 'Alexander Kimmig', this.image, true)
}
}
}
</script>

+ 241
- 0
client/src/views/Register.vue Просмотреть файл

@@ -0,0 +1,241 @@
<template>
<v-container
fluid
tag="section"
>
<v-card
v-if="registered"
flat
>
<h1>Registrierung abgeschlossen</h1>
<p>Vielen Dank für Ihre Registrierung</p>
<p>Sie erhalten eine E-Mail mit einem Link zur Bestätigung der Registrierung.</p>
<p style="font-style: italic;">Sollten Sie keine E-Mail erhalten, so sehen Sie bitte auch im Spam-Ordner nach!</p>
</v-card>
<v-card
v-else
flat
>
<h1>Registrierung</h1>
<p>Hier können Sie sich kostenlos für das System registrieren.</p>
<p>Anmerkung: School-in-Motion ist in erster Linie für Schulen konzipiert. Wird eine Registrierung missbräuchlich verwendet, so behalten wir uns vor, den Nutzer zu sperren bzw. zu löschen.</p>
<v-row>
<v-col
cols="12"
>
<v-text-field
v-model="plz"
label="Postleitzahl"
/>
</v-col>
<v-col
cols="12"
>
<v-select
v-model="schule"
:items="organizer"
label="Schule auswählen"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-if="schule && schule !== -1"
:value="schule.name"
label="Schulname"
disabled
/>
<v-text-field
v-else
v-model="name"
label="Schulname"
:disabled="schule !== -1"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-if="schule && schule !== -1"
:value="schule.ort"
label="Ort/Stadt"
disabled
/>
<v-text-field
v-else
v-model="ort"
label="Ort/Stadt"
:disabled="schule !== -1"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-model="givenName"
label="Vorname"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-model="familyName"
label="Nachname"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="email"
label="E-Mail-Adresse"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-model="password"
type="password"
label="Passwort"
/>
</v-col>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-model="password2"
type="password"
label="Passwort wiederholen"
/>
</v-col>
<v-col
cols="12"
>
<v-btn
block
@click="register"
>
Registrieren
</v-btn>
</v-col>
</v-row>
</v-card>
</v-container>
</template>

<script>
import { useGraphQL } from '@/plugins/graphql'
import gql from 'graphql-tag'

export default {
name: 'PagesRegister',

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

data: () => ({
plz: null,
ort: null,
name: null,
givenName: null,
familyName: null,
email: null,
password: null,
password2: null,
schule: null,
OrganizerFind: [],
registered: false
}),

computed: {
organizer () {
return [{ value: null, text: 'bitte Schule auswählen...', disabled: true }, ...this.OrganizerFind.map(o => ({ value: o, text: o.name })), { value: -1, text: 'neue Schule anlegen' }]
}
},

methods: {
async register () {
const err = []
const EMAIL_ADDRESS_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

if (!this.plz) err.push('Postleitzahl fehlt!')
if (isNaN(parseInt(this.plz))) err.push('Postleitzahl darf nur aus Ziffern bestehen!')
if (!this.schule) err.push('Schule fehlt!')
if (this.schule === -1 && !this.name) err.push('Schulname fehlt!')
if (this.schule === -1 && !this.ort) err.push('Schulort fehlt!')
if (!this.givenName) err.push('Vorname fehlt!')
if (!this.familyName) err.push('Nachname fehlt!')
if (!this.email) err.push('E-Mail-Adresse fehlt!')
if (this.email && !EMAIL_ADDRESS_REGEX.test(this.email)) err.push('E-Mail-Adresse ungültig!')
if (!this.password) err.push('Passwort fehlt!')
if (this.password !== this.password2) err.push('Passwörter stimmen nicht überein!')

if (err.length !== 0) {
this.$store.commit('OPEN_SNACKBAR', `<ul><li>${err.join('</li><li>')}</li></ul>`)
return
}

let schulid = null

if (this.schule === -1) {
const tmp = await this.$apollo.mutate({
mutation: gql`mutation($name: String!, $plz: Int!, $ort: String!) {
OrganizerRegister(name: $name, plz: $plz, ort: $ort) { _id }
}`,
variables: {
name: this.name,
plz: parseInt(this.plz),
ort: this.ort
}
})
schulid = tmp.data.OrganizerRegister._id
} else {
schulid = this.schule._id
}

await this.$apollo.mutate({
mutation: gql`mutation($organizer: UUID!, $givenName: String!, $familyName: String!, $email: EmailAddress!, $passwort: String!) {
PersonRegister(organizer: $organizer, givenName: $givenName, familyName: $familyName, email: $email, passwort: $passwort) { _id }
}`,
variables: {
organizer: schulid,
givenName: this.givenName,
familyName: this.familyName,
email: this.email,
passwort: this.password
}
})

this.registered = true
}
},

watch: {
plz (v) {
const tmp = parseInt(v)
if (isNaN(tmp)) return

this.$apollo.query({
query: gql`query($plz: Int!) { OrganizerFind(plz: $plz) { _id name plz ort } }`,
variables: {
plz: tmp
}
}).then((r) => {
this.OrganizerFind = r.data.OrganizerFind
})
}
}
}
</script>

+ 208
- 0
client/src/views/Reset.vue Просмотреть файл

@@ -0,0 +1,208 @@
<template>
<v-container
id="login"
class="fill-height justify-center"
tag="section"
>
<v-row justify="center">
<base-material-card
v-if="resetCode === null"
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">
Passwort zurücksetzen
</h1>
</div>
</template>

<v-card-text class="text-center">
<v-row>
<v-col
cols="12"
>
<v-text-field
v-model="email"
color="red"
label="E-Mail..."
prepend-icon="mdi-email"
name="email"
@keydown.enter="reset"
/>
</v-col>

<v-col
cols="12"
>
<v-btn
class="mb-5"
color="red"
tile
@click="reset"
width="100%"
>
<v-icon class="mr-4">
fa-sign-in-alt
</v-icon>
Passwort zurücksetzen
</v-btn>
</v-col>
</v-row>
</v-card-text>
</base-material-card>
<base-material-card
v-else
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">
Passwort zurücksetzen
</h1>
</div>
</template>

<v-card-text class="text-center">
<v-row>
<v-col
cols="12"
>
<v-text-field
v-model="email"
color="red"
label="E-Mail-Adresse"
prepend-icon="far fa-envelope"
name="email"
@keydown.enter="reset"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="resetCode"
label="Code"
prepend-icon="far fa-key-skeleton"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="newPassword"
type="password"
label="neues Passwort"
prepend-icon="far fa-key"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="newPassword2"
type="password"
label="Passwort wiederholen"
prepend-icon="far fa-key"
/>
</v-col>
<v-col
cols="12"
>
<v-btn
class="mb-5"
color="red"
tile
block
@click="reset"
>
<v-icon class="mr-4">
fa-sign-in-alt
</v-icon>
Passwort zurücksetzen
</v-btn>
</v-col>
</v-row>
</v-card-text>
</base-material-card>
</v-row>
</v-container>
</template>

<script>
import { useGraphQL } from '@/plugins/graphql'
import gql from 'graphql-tag'

export default {
name: 'PagesLogin',

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

props: {
code: {
type: String,
required: false,
default: null
}
},

data: () => ({
valid: true,
email: '',
resetCode: null,
newPassword: null,
newPassword2: null
}),

methods: {
reset () {
const err = []
const EMAIL_ADDRESS_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

if (!this.email) err.push('E-Mail-Adresse fehlt!')
if (this.email && !EMAIL_ADDRESS_REGEX.test(this.email)) err.push('E-Mail-Adresse ungültig!')
if (this.resetCode && !this.newPassword) err.push('Passwort fehlt!')
if (this.newPassword !== this.newPassword2) err.push('Passwörter stimmen nicht überein!')

if (err.length !== 0) {
this.$store.commit('OPEN_SNACKBAR', `<ul><li>${err.join('</li><li>')}</li></ul>`)
return
}

this.$apollo.mutate({
mutation: gql`mutation($email: EmailAddress!, $resetCode: String, $newPassword: String) {
PersonPasswordReset(email: $email, resetCode: $resetCode, newPassword: $newPassword)
}`,
variables: {
email: this.email,
resetCode: this.resetCode || null,
newPassword: this.newPassword || null
}
}).then(ret => {
this.resetCode = ''
if (this.resetCode !== null && this.newPassword !== null && ret?.data?.PersonPasswordReset === true) {
this.$router.replace('/login')
}
}).catch(e => {
this.$store.commit('OPEN_SNACKBAR', e)
})
}
},

mounted () {
if (this.code) this.resetCode = this.code
}
}
</script>

+ 3
- 2
client/src/views/components/core/Drawer.vue Просмотреть файл

@@ -19,8 +19,9 @@

<v-list-item two-line>
<v-list-item-content>
<v-list-item-title class="text-uppercase font-weight-regular display-2">
<span class="logo-normal">Turnen auf Zeit</span>
<v-list-item-avatar><img src="../../../assets/bogen.png"/></v-list-item-avatar>
<v-list-item-title class="font-weight-regular display-2 text-center">
<span class="logo-normal">schoolINmotion</span>
</v-list-item-title>
</v-list-item-content>
</v-list-item>

+ 109
- 1
client/src/views/components/event/event.vue Просмотреть файл

@@ -23,6 +23,11 @@
>
Rangliste
</v-tab>
<v-tab
href="#urkunden"
>
Urkunden
</v-tab>
</v-tabs>
<v-tabs-items
v-model="tab"
@@ -472,6 +477,93 @@
</template>
</v-data-table>
</v-tab-item>
<v-tab-item
value="urkunden"
>
<v-card
flat
>
<v-row>
<v-col
cols="12"
xl="6"
order-xl="2"
>
<v-row>
<v-col cols="12">
<v-btn
block
@click="generatePDF(true)"
>
Urkunden inklusive Hintergrund
</v-btn>
</v-col>
<v-col cols="12">
<v-btn
block
@click="generatePDF(false)"
>
Urkunden für Vordruck
</v-btn>
</v-col>
</v-row>
</v-col>
<v-col
cols="12"
xl="6"
order-xl="1"
>
<v-row>
<v-col cols="6">
<v-text-field
v-model="urkunde.name"
label="Name auf Urkunde"
/>
</v-col>
<v-col cols="6">
<v-btn
fab
small
@click="urkunde.name = person"
class="mr-2"
>
<v-icon>far fa-long-arrow-alt-left</v-icon>
</v-btn>
übernehme &quot;{{ person }}&quot;
</v-col>
<v-col cols="6">
<v-text-field
v-model="urkunde.schule"
label="Organisator auf Urkunde"
/>
</v-col>
<v-col cols="6">
<v-btn
fab
small
@click="urkunde.schule = organizer"
class="mr-2"
>
<v-icon>far fa-long-arrow-alt-left</v-icon>
</v-btn>
übernehme &quot;{{ organizer }}&quot;
</v-col>
<v-col cols="12">
Unterschrift auf Urkunde:<br>
<v-image-input
v-model="urkunde.unterschrift"
clearable
:image-height="256"
:image-width="512"
image-min-scaling="contain"
hide-actions
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card>
</v-tab-item>
</v-tabs-items>
</v-container>
</template>
@@ -479,9 +571,10 @@
<script>
import gql from 'graphql-tag'
import moment from 'moment'
import { generateUrkunde } from '@/urkunde'

const timeslotquery = '_id time duration team { _id name } result { bonus runtime calctime results { _id teile bonus wechsel } }'
const query = `_id name date _organizer apparatus { _apparatus apparatus { name logo } elements bonus malus } timeslots { ${timeslotquery} }`
const query = `_id name date _organizer organizer { name } apparatus { _apparatus apparatus { name logo } elements bonus malus } timeslots { ${timeslotquery} }`

export default {
name: 'Event',
@@ -577,6 +670,11 @@ export default {
sortable: false
}
]
},
urkunde: {
name: '',
schule: '',
unterschrift: null
}
}),

@@ -634,6 +732,12 @@ export default {
}
return acc
}, []).sort((a, b) => a.time < b.time ? -1 : 1).map((m, i) => ({ ...m, platz: i + 1 }))
},
organizer () {
return this.Event?.organizer?.name
},
person () {
return `${this.$store.getters.profile?.givenName} ${this.$store.getters.profile?.familyName}`
}
},

@@ -824,6 +928,10 @@ export default {
})

this.resultclose()
},
async generatePDF (vorlage = false) {
console.log(this.$store.getters.profile)
generateUrkunde(this.ergebnisse.map(e => ({ platz: `${e.platz}. PLATZ`, zeit: `${Math.floor(e.time / 60)} MIN. ${Math.floor(e.time % 60)} SEK.`, team: e.team })), this.urkunde.schule, this.urkunde.name, this.urkunde.unterschrift, vorlage)
}
},


+ 41
- 0
client/src/views/components/start.vue Просмотреть файл

@@ -0,0 +1,41 @@
<template>
<v-container
fluid
tag="section"
>
<img
src="../../assets/logo_sim.jpg"
/>
<h2><b>schoolINmotion</b> ist ein Mannschaftswettbewerb, bei dem eine Anzahl von zwei bis vier Schülerinnen und Schülern in einem Team antreten. Es ist dabei unerheblich, ob das Team aus nur Jungen, nur Mädchen oder als gemischtes Team zusammengestellt wird.</h2>
<h3>Ziel des Wettbewerbs ist es, eine möglichst schnelle Zeit in einem Geräte-Parcours zu erzielen. Zusätzlich sind an den einzelnen Geräten turnerische Elemente zu erfüllen.</h3>
<h3>Der zu absolvierende Parcours besteht aus 4 Geräten, an welchen jeweils drei Elemente gezeigt werden müssen. Jedes Teammitglied kann eine oder zwei Geräte turnen, allerdings nicht direkt hintereinander. Es zählt die Team-Zeit, nicht die beste Ausführung! Auf turnerische Aspekte, wie die korrekte Ausführung von Pflichtübungen wird bewusst weniger Wert gelegt. So garantiert das gemeinsame Turnen Spaß und Action.</h3>
<p style="clear: both;"></p>
</v-container>
</template>

<script>
export default {
name: 'start'
}
</script>

<style scoped>
img {
width: 400px;
height: auto;
float: right;
margin: 0px 24px;
}

h2 {
line-height: 180%;
margin-bottom: 16px;
letter-spacing: 0.2px;
}

h3 {
line-height: 180%;
letter-spacing: 0.2px;
margin-bottom: 12px;
}
</style>

+ 1
- 0
server/.gitignore Просмотреть файл

@@ -34,3 +34,4 @@ lerna-debug.log*
!.vscode/extensions.json

/assets/keys
/src/env.ts

+ 133
- 0
server/package-lock.json Просмотреть файл

@@ -21,6 +21,8 @@
"js-base64": "^3.6.0",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.6.6",
"nodemailer": "^6.6.3",
"nodemailer-smtp-transport": "^2.7.4",
"prompt-async": "^0.9.9",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
@@ -6550,6 +6552,26 @@
"npm": ">=1.3.7"
}
},
"node_modules/httpntlm": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
"integrity": "sha1-rQFScUOi6Hc8+uapb1hla7UqNLI=",
"dependencies": {
"httpreq": ">=0.4.22",
"underscore": "~1.7.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/httpreq": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.5.2.tgz",
"integrity": "sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw==",
"engines": {
"node": ">= 6.15.1"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
@@ -8840,6 +8862,42 @@
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"node_modules/nodemailer": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz",
"integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemailer-fetch": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz",
"integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q="
},
"node_modules/nodemailer-shared": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz",
"integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=",
"dependencies": {
"nodemailer-fetch": "1.6.0"
}
},
"node_modules/nodemailer-smtp-transport": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz",
"integrity": "sha1-DYmvAZoUSkgP2OzJmZfZ+DjxNoU=",
"dependencies": {
"nodemailer-shared": "1.1.0",
"nodemailer-wellknown": "0.1.10",
"smtp-connection": "2.12.0"
}
},
"node_modules/nodemailer-wellknown": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
"integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U="
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -10849,6 +10907,15 @@
"npm": ">= 3.0.0"
}
},
"node_modules/smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
"integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=",
"dependencies": {
"httpntlm": "1.6.1",
"nodemailer-shared": "1.1.0"
}
},
"node_modules/snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -12267,6 +12334,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/underscore": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk="
},
"node_modules/union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -18289,6 +18361,20 @@
"sshpk": "^1.7.0"
}
},
"httpntlm": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
"integrity": "sha1-rQFScUOi6Hc8+uapb1hla7UqNLI=",
"requires": {
"httpreq": ">=0.4.22",
"underscore": "~1.7.0"
}
},
"httpreq": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.5.2.tgz",
"integrity": "sha512-2Jm+x9WkExDOeFRrdBCBSpLPT5SokTcRHkunV3pjKmX/cx6av8zQ0WtHUMDrYb6O4hBFzNU6sxJEypvRUVYKnw=="
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
@@ -20030,6 +20116,39 @@
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"nodemailer": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.3.tgz",
"integrity": "sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew=="
},
"nodemailer-fetch": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz",
"integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q="
},
"nodemailer-shared": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz",
"integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=",
"requires": {
"nodemailer-fetch": "1.6.0"
}
},
"nodemailer-smtp-transport": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz",
"integrity": "sha1-DYmvAZoUSkgP2OzJmZfZ+DjxNoU=",
"requires": {
"nodemailer-shared": "1.1.0",
"nodemailer-wellknown": "0.1.10",
"smtp-connection": "2.12.0"
}
},
"nodemailer-wellknown": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
"integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U="
},
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -21553,6 +21672,15 @@
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz",
"integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw=="
},
"smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
"integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=",
"requires": {
"httpntlm": "1.6.1",
"nodemailer-shared": "1.1.0"
}
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@@ -22670,6 +22798,11 @@
"which-boxed-primitive": "^1.0.2"
}
},
"underscore": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk="
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",

+ 2
- 0
server/package.json Просмотреть файл

@@ -33,6 +33,8 @@
"js-base64": "^3.6.0",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.6.6",
"nodemailer": "^6.6.3",
"nodemailer-smtp-transport": "^2.7.4",
"prompt-async": "^0.9.9",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",

+ 1
- 0
server/schema.gql Просмотреть файл

@@ -109,6 +109,7 @@ type Mutation {
login(token: String, passwort: String, email: String): Person!
PersonRegister(passwort: String!, email: EmailAddress!, familyName: String!, givenName: String!, organizer: UUID!): Person!
PersonConfirmMail(confirmCode: String!, email: String!): Person
PersonPasswordReset(newPassword: String, resetCode: String, email: EmailAddress!): Boolean
ChangePassword(newPassword: String!, oldPassword: String!): Boolean!
PersonUpdate(master: Boolean, email: EmailAddress, familyName: String, givenName: String, id: UUID!): Person!
PersonCreate(master: Boolean, email: EmailAddress!, familyName: String!, givenName: String!): Person!

+ 54
- 0
server/src/config.ts Просмотреть файл

@@ -0,0 +1,54 @@
import {
DBuser,
DBpwd,
DBhost,
DBport,
DBsource,
DBdb,
MAILhost,
MAILport,
MAILsecure,
MAILuser,
MAILpwd,
SERVERport,
SERVERlisten,
SERVERhost,
ADMINmail,
MAILaddress, MAILname, SYSTEMname
} from './env';

export const SYSDATA = {
name: SYSTEMname,
}

export const DBDATA = {
user: DBuser,
pwd: DBpwd,
host: DBhost,
port: DBport,
source: DBsource,
db: DBdb,
};

export const SMTPDATA = {
host: MAILhost,
port: MAILport,
secure: MAILsecure,
user: MAILuser,
pwd: MAILpwd,
};

export const MAILDATA = {
address: MAILaddress,
name: MAILname,
}

export const SERVERDATA = {
port: SERVERport, // 3000
listen: SERVERlisten, // '127.0.0.1'
host: SERVERhost, // 'https://www.selbsttest-schule.de'
};

export const ADMINDATA = {
mail: ADMINmail,
};

+ 5
- 12
server/src/db.ts Просмотреть файл

@@ -2,16 +2,9 @@ import * as MongoClient from 'mongodb';
import { Client } from './client';
import { UUID } from './global/scalars/UUID';

export class DB {
private readonly DBDATA = {
user: 'root',
pwd: 'TurnenAufZeit',
host: 'localhost',
port: 27017,
source: 'admin',
db: 'TurnenAufZeit',
};
import { DBDATA } from './config';

export class DB {
private mongo: MongoClient;
private db: MongoClient.Db;

@@ -21,8 +14,8 @@ export class DB {

public async connect(): MongoClient.Db {
this.mongo = await new Promise((resolve, reject) => {
MongoClient.connect(`mongodb://${this.DBDATA.user}:${this.DBDATA.pwd}@${this.DBDATA.host}:${this.DBDATA.port}`, {
authSource: this.DBDATA.source,
MongoClient.connect(`mongodb://${DBDATA.user}:${DBDATA.pwd}@${DBDATA.host}:${DBDATA.port}`, {
authSource: DBDATA.source,
useNewUrlParser: true,
useUnifiedTopology: true,
poolSize: 20,
@@ -34,7 +27,7 @@ export class DB {
});
});

this.db = this.mongo.db(this.DBDATA.db);
this.db = this.mongo.db(DBDATA.db);
}

public collection(ops): MongoClient.Collection {

+ 40
- 0
server/src/mail.ts Просмотреть файл

@@ -0,0 +1,40 @@
import * as nodemailer from 'nodemailer';
import * as smtpMail from 'nodemailer-smtp-transport';

import { SMTPDATA, MAILDATA } from './config'
import {EmailAddress} from './global/scalars/EmailAddress'

export const sendmail = (to: EmailAddress, subject: string, text: string, cc?: string, bcc?: string, from?: string) => {
const transporter = nodemailer.createTransport(smtpMail({
host: SMTPDATA.host,
port: SMTPDATA.port,
secure: SMTPDATA.secure, // use TLS
auth: {
user: SMTPDATA.user,
pass: SMTPDATA.pwd,
},
tls: {
rejectUnauthorized: false
}
}))

const sender = `"${MAILDATA.name}" <${MAILDATA.address}>`;
const mailOptions: any = {
from: sender,
to,
cc: cc ? cc : '',
bcc: bcc ? bcc : '',
subject,
text,
};

if(from) {
mailOptions.replyTo = from;
}

transporter.sendMail(mailOptions, (err) => {
if (err) {
console.log('Fehler: ' + err);
}
})
}

+ 3
- 1
server/src/main.ts Просмотреть файл

@@ -4,6 +4,8 @@ import { AppModule } from './app.module';
import { db } from './db';
import { initDB } from './init';

import { SERVERDATA } from './config';

import { PubSub } from 'graphql-subscriptions';
import {NestExpressApplication} from '@nestjs/platform-express'

@@ -14,7 +16,7 @@ async function bootstrap(argv: string[]) {
await initDB(argv.findIndex(a => a.toLowerCase() === 'init') !== -1, argv.findIndex(a => a.toLowerCase() === 'reset') !== -1);

const app = await NestFactory.create<NestExpressApplication>(AppModule);
await app.listen(3000);
await app.listen(SERVERDATA.port, SERVERDATA.listen);
}

let args: string[];

+ 69
- 2
server/src/person/resolver/person.mutation.ts Просмотреть файл

@@ -8,6 +8,9 @@ import {OrganizerService} from '../../organizer/organizer.service'
import {EmailAddress} from '../../global/scalars/EmailAddress'
import {checkPassword, secureHash} from '../../generate'
import {pubsub} from '../../main'
import {sendmail} from '../../mail'
import {SERVERDATA, SYSDATA} from '../../config'
import { v4 as uuid } from 'uuid'

@Resolver(() => Person)
export class PersonResolverM {
@@ -56,17 +59,42 @@ export class PersonResolverM {

const tmp = await this.service.create(client, givenName, familyName, email, passwort);

let isAdmin = false;

if (!o._admins) {
await organizerService.update(client, o._id, {$set: {_admins: [ tmp._id ] }}, {});
isAdmin = true;
} else if (o._admins.length === 0) {
await organizerService.update(client, o._id, {$push: {_admins: tmp._id }}, {});
isAdmin = true;
} else if (!o._pending) {
await organizerService.update(client, o._id, {$set: {_pending: [ tmp._id ] }}, {});
} else {
await organizerService.update(client, o._id, {$push: {_pending: tmp._id }}, {});
}

// TODO: Mail verschicken
const link = `${SERVERDATA.host}/confirm/${(tmp as unknown as any).confirmCode}`
const text = `Vielen Dank für Ihre Registrierung bei ${SYSDATA.name}

Bitte bestätigen Sie unter folgendem Link Ihre Registrierung:

${link}` + (isAdmin?'':`

Sie müssen außerdem zunächst von einem Administrator Ihrer Schule/Ihres Vereins freigeschaltet werden. Dieser erhält ebenfalls eine E-Mail über Ihre Registrierung.`);

sendmail(email,`[${SYSDATA.name}] Neu-Registrierung`, text);

if (!isAdmin) {
const admintext = `${givenName} ${familyName} hat sich neu bei ${SYSDATA.name} für Ihre Schule/Ihren Verein ${o.name} registriert.

Bitte melden Sie sich unter ${SERVERDATA.host} an und bearbeiten die Registrierung.`

for (let i = 0; i < o._admins?.length; i++) {
const p = await this.service.findOneById(o._admins[i]);

sendmail(p.email, `[${SYSDATA.name}] Neu-Registrierung für ${o.name}`, admintext);
}
}

pubsub.publish('PersonUpdated', { PersonUpdated: tmp });

@@ -82,13 +110,52 @@ export class PersonResolverM {
const tmp = await this.service.find({email, confirmCode});

if (tmp.length !== 1) {
throw new HttpException('confirmCode not correct', 403);
throw new HttpException('E-Mail-Adresse oder Bestätigungscode ungültig', 403);
}

this.service.update(client, tmp[0]._id, { $unset: { confirmCode } })
return tmp[0];
}

@Mutation(() => Boolean, { nullable: true })
async PersonPasswordReset(
@Context('client') client: Client,
@Args('email') email: EmailAddress,
@Args('resetCode', { type: () => String, nullable: true }) resetCode?: string,
@Args('newPassword', { type: () => String, nullable: true }) newPassword?: string,
): Promise<boolean> {
const tmp = await this.service.find({email});

console.log((tmp[0] as unknown as any).resetCode)
console.log(resetCode)

if (tmp.length === 1) {
if (!resetCode && !newPassword) {
const code = uuid();
this.service.update(client, tmp[0]._id, {$set: {resetCode: code}})

const link = `${SERVERDATA.host}/reset/${code}`
const text = `Unter folgendem Link können Sie Ihr Passwort zurücksetzen:

${link}

Alternativ können Sie auch den folgenden Code in das Eingabefeld kopieren:

${code}`

console.log(text);

sendmail(email,`[${SYSDATA.name}] Passwort zurücksetzen`, text);

} else if ((tmp[0] as unknown as any).resetCode === resetCode) {
this.service.update(client, tmp[0]._id, {$set: { passwort: await secureHash(newPassword) }, $unset: { resetCode: '' } });
return true;
}
}

return null;
}

@Mutation(() => Boolean, { nullable: false })
async ChangePassword(
@Context('client') client: Client,

Загрузка…
Отмена
Сохранить