mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into bugfix_database_downgrade_and_upgrade_again
This commit is contained in:
commit
b4feddea4b
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -441,7 +441,7 @@ jobs:
|
||||
report_name: Coverage Admin Interface
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 65
|
||||
min_coverage: 50
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -491,7 +491,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 38
|
||||
min_coverage: 39
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
3
admin/.env.dist
Normal file
3
admin/.env.dist
Normal file
@ -0,0 +1,3 @@
|
||||
GRAPHQL_URI=http://localhost:4000/graphql
|
||||
WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1
|
||||
DEBUG_DISABLE_AUTH=false
|
||||
@ -1,6 +1,11 @@
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,vue}',
|
||||
'!**/node_modules/**',
|
||||
'!src/assets/**',
|
||||
'!**/?(*.)+(spec|test).js?(x)',
|
||||
],
|
||||
moduleFileExtensions: [
|
||||
'js',
|
||||
// 'jsx',
|
||||
|
||||
@ -32,15 +32,20 @@
|
||||
"core-js": "^3.6.5",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"graphql": "^15.6.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "26.6.3",
|
||||
"moment": "^2.29.1",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apollo": "^3.0.8",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.5.3",
|
||||
"vuex": "^3.6.2"
|
||||
"vue-toasted": "^1.1.28",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.8",
|
||||
|
||||
BIN
admin/public/img/brand/green.png
Normal file
BIN
admin/public/img/brand/green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@ -1,43 +1,28 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import App from './App'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
const stubs = {
|
||||
RouterView: true,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
token: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store = {}
|
||||
|
||||
return {
|
||||
getItem: (key) => {
|
||||
return store[key] || null
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
store[key] = value.toString()
|
||||
},
|
||||
removeItem: (key) => {
|
||||
delete store[key]
|
||||
},
|
||||
clear: () => {
|
||||
store = {}
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
describe('App', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(App, { localVue, mocks })
|
||||
return shallowMount(App, { localVue, stubs, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
describe('shallowMount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
@ -46,23 +31,4 @@ describe('App', () => {
|
||||
expect(wrapper.find('div#app').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('window localStorage is undefined', () => {
|
||||
it('does not commit a token to the store', () => {
|
||||
expect(storeCommitMock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with token in local storage', () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
})
|
||||
window.localStorage.setItem('vuex', JSON.stringify({ token: 1234 }))
|
||||
})
|
||||
|
||||
it.skip('commits the token to the store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('token', 1234)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<default-layout v-if="$store.state.token" />
|
||||
<router-view v-else></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import defaultLayout from '@/layouts/defaultLayout.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
name: 'app',
|
||||
components: { defaultLayout },
|
||||
}
|
||||
</script>
|
||||
|
||||
1
admin/src/assets/mocks/styleMock.js
Normal file
1
admin/src/assets/mocks/styleMock.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {}
|
||||
15
admin/src/components/ContentFooter.vue
Normal file
15
admin/src/components/ContentFooter.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<hr />
|
||||
<br />
|
||||
<div class="text-center">
|
||||
Gradido Akademie Adminkonsole
|
||||
<div><small>Version: 0.0.1</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContentFooter',
|
||||
}
|
||||
</script>
|
||||
174
admin/src/components/CreationFormular.spec.js
Normal file
174
admin/src/components/CreationFormular.spec.js
Normal file
@ -0,0 +1,174 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationFormular from './CreationFormular.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
verifyLogin: {
|
||||
name: 'success',
|
||||
id: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
$apollo: {
|
||||
query: apolloMock,
|
||||
},
|
||||
$store: {
|
||||
commit: stateCommitMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
type: '',
|
||||
item: {},
|
||||
creation: [],
|
||||
itemsMassCreation: {},
|
||||
}
|
||||
|
||||
describe('CreationFormular', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CreationFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-creation-formular', () => {
|
||||
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('server sends back moderator data', () => {
|
||||
it('called store commit with mocked data', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('moderator', { name: 'success', id: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('server throws error for moderator data call', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
apolloMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
it('has called store commit with fake data', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('moderator', { id: 0, name: 'Test Moderator' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('radio buttons to selcet month', () => {
|
||||
it('has three radio buttons', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||
})
|
||||
|
||||
describe('with mass creation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ type: 'massCreation' })
|
||||
})
|
||||
|
||||
describe('first radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 0', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([0])],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('second radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 1', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([1])],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('third radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 2', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([2])],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with single creation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
})
|
||||
|
||||
describe('first radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 200', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('second radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 400', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('third radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 400', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(600)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
317
admin/src/components/CreationFormular.vue
Normal file
317
admin/src/components/CreationFormular.vue
Normal file
@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="component-creation-formular">
|
||||
<div>
|
||||
<h3>
|
||||
{{
|
||||
this.type === 'singleCreation'
|
||||
? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + ''
|
||||
: 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
|
||||
}}
|
||||
</h3>
|
||||
<div v-show="this.type === 'massCreation' && Object.keys(this.itemsMassCreation).length <= 0">
|
||||
Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="this.type === 'singleCreation' || Object.keys(this.itemsMassCreation).length > 0"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
>
|
||||
<b-form ref="creationForm">
|
||||
<b-row class="m-4">
|
||||
<label>Monat Auswählen</label>
|
||||
<b-col class="text-left">
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="beforeLastMonth"
|
||||
:disabled="creation[0] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
|
||||
>
|
||||
{{ beforeLastMonth.short }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }}
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="lastMonth"
|
||||
:disabled="creation[1] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(lastMonth, 1, creation[1])"
|
||||
>
|
||||
{{ lastMonth.short }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }}
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="currentMonth"
|
||||
:disabled="creation[2] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(currentMonth, 2, creation[2])"
|
||||
>
|
||||
{{ currentMonth.short }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }}
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="m-4" v-show="createdIndex">
|
||||
<label>Betrag Auswählen</label>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
<b-form-input
|
||||
type="number"
|
||||
v-model="value"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
|
||||
<b-form-input
|
||||
type="range"
|
||||
v-model="value"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
step="10"
|
||||
@load="checkFormForUpdate('range')"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<label>Text eintragen</label>
|
||||
<div>
|
||||
<b-form-textarea
|
||||
id="textarea-state"
|
||||
v-model="text"
|
||||
:state="text.length >= 10"
|
||||
placeholder="Mindestens 10 Zeichen eingeben"
|
||||
@load="checkFormForUpdate('text')"
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-center">
|
||||
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
|
||||
zurücksetzen
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-center">
|
||||
<div class="text-right">
|
||||
<b-button
|
||||
v-if="pagetype === 'PageCreationConfirm'"
|
||||
type="button"
|
||||
variant="success"
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
Update Schöpfung ({{ type }},{{ pagetype }})
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
type="button"
|
||||
variant="success"
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
Schöpfung einreichen ({{ type }})
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { verifyLogin } from '../graphql/verifyLogin'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
export default {
|
||||
name: 'CreationFormular',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
pagetype: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
creationUserData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
creation: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
itemsMassCreation: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
radioSelected: '',
|
||||
text: '',
|
||||
value: 0,
|
||||
rangeMin: 0,
|
||||
rangeMax: 1000,
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
long: this.$moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
|
||||
},
|
||||
submitObj: null,
|
||||
isdisabled: true,
|
||||
createdIndex: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Auswählen eines Zeitraumes
|
||||
updateRadioSelected(name, index, openCreation) {
|
||||
this.createdIndex = index
|
||||
// Wenn Mehrfachschöpfung
|
||||
if (this.type === 'massCreation') {
|
||||
// An Creation.vue emitten und radioSelectedMass aktualisieren
|
||||
this.$emit('update-radio-selected', [name, index])
|
||||
}
|
||||
// Wenn Einzelschöpfung
|
||||
if (this.type === 'singleCreation') {
|
||||
this.rangeMin = 0
|
||||
// Der maximale offene Betrag an GDD die für ein User noch geschöpft werden kann
|
||||
this.rangeMax = openCreation
|
||||
}
|
||||
},
|
||||
checkFormForUpdate(input) {
|
||||
switch (input) {
|
||||
case 'text':
|
||||
this.text = this.creationUserData.text
|
||||
break
|
||||
case 'range':
|
||||
this.value = this.creationUserData.creationGdd
|
||||
break
|
||||
default:
|
||||
// TODO: Toast
|
||||
alert("I don't know such values")
|
||||
}
|
||||
},
|
||||
submitCreation() {
|
||||
// Formular Prüfen ob ein Zeitraum ausgewählt wurde. Ansonsten abbrechen und Hinweis anzeigen
|
||||
if (this.radioSelected === '') {
|
||||
return alert('Bitte wähle einen Zeitraum!')
|
||||
}
|
||||
// Formular Prüfen ob der GDD Betrag grösser 0 ist. Ansonsten abbrechen und Hinweis anzeigen
|
||||
if (this.value === 0) {
|
||||
return alert('Bitte gib einen GDD Betrag an!')
|
||||
}
|
||||
// Formular Prüfen ob der Text vorhanden ist. Ansonsten abbrechen und Hinweis anzeigen
|
||||
if (this.text === '') {
|
||||
return alert('Bitte gib einen Text ein!')
|
||||
}
|
||||
// Formular Prüfen ob der Text länger als 10 Zeichen hat. Ansonsten abbrechen und Hinweis anzeigen
|
||||
if (this.text.length < 10) {
|
||||
return alert('Bitte gib einen Text ein der länger als 10 Zeichen ist!')
|
||||
}
|
||||
if (this.type === 'massCreation') {
|
||||
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
|
||||
const i = Object.keys(this.itemsMassCreation).length
|
||||
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
|
||||
alert('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
|
||||
this.submitObj = [
|
||||
{
|
||||
item: this.itemsMassCreation,
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
amount: this.value,
|
||||
memo: this.text,
|
||||
moderator: this.$store.state.moderator.id,
|
||||
},
|
||||
]
|
||||
alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
|
||||
|
||||
// $store - offene Schöpfungen hochzählen
|
||||
this.$store.commit('openCreationsPlus', i)
|
||||
|
||||
// lösche alle Mitglieder aus der MehrfachSchöpfungsListe nach dem alle Mehrfachschpfungen zum bestätigen gesendet wurden.
|
||||
this.$emit('remove-all-bookmark')
|
||||
}
|
||||
|
||||
if (this.type === 'singleCreation') {
|
||||
this.submitObj = {
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
}
|
||||
|
||||
if (this.pagetype === 'PageCreationConfirm') {
|
||||
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
|
||||
alert('UPDATE EINZEL SCHÖPFUNG ABSENDEN FÜR >> ')
|
||||
// umschreiben, update eine bestehende Schöpfung eine
|
||||
this.$emit('update-creation-data', {
|
||||
datum: this.radioSelected.long,
|
||||
creationGdd: this.value,
|
||||
text: this.text,
|
||||
})
|
||||
} else {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreation,
|
||||
variables: this.submitObj,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$toasted.success(
|
||||
`Offene schöpfung (${this.value} GDD) für ${this.item.email} wurde gespeichert, liegen zur bestätigung bereit`,
|
||||
)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.submitObj = null
|
||||
this.createdIndex = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.submitObj = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
searchModeratorData() {
|
||||
this.$apollo
|
||||
.query({ query: verifyLogin })
|
||||
.then((result) => {
|
||||
this.$store.commit('moderator', result.data.verifyLogin)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$store.commit('moderator', { id: 0, name: 'Test Moderator' })
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.searchModeratorData()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
68
admin/src/components/NavBar.spec.js
Normal file
68
admin/src/components/NavBar.spec.js
Normal file
@ -0,0 +1,68 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import NavBar from './NavBar.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeDispatchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
state: {
|
||||
openCreations: 1,
|
||||
token: 'valid-token',
|
||||
},
|
||||
dispatch: storeDispatchMock,
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('NavBar', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(NavBar, { mocks, localVue })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-nabvar', () => {
|
||||
expect(wrapper.find('.component-nabvar').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('wallet', () => {
|
||||
const assignLocationSpy = jest.fn()
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('a').at(5).trigger('click')
|
||||
})
|
||||
|
||||
it.skip('changes widnow location to wallet', () => {
|
||||
expect(assignLocationSpy).toBeCalledWith('valid-token')
|
||||
})
|
||||
|
||||
it('dispatches logout to store', () => {
|
||||
expect(storeDispatchMock).toBeCalledWith('logout')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout', () => {
|
||||
// const assignLocationSpy = jest.fn()
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('a').at(6).trigger('click')
|
||||
})
|
||||
|
||||
it('redirects to /logout', () => {
|
||||
expect(routerPushMock).toBeCalledWith('/logout')
|
||||
})
|
||||
|
||||
it('dispatches logout to store', () => {
|
||||
expect(storeDispatchMock).toBeCalledWith('logout')
|
||||
})
|
||||
})
|
||||
})
|
||||
51
admin/src/components/NavBar.vue
Normal file
51
admin/src/components/NavBar.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="component-nabvar">
|
||||
<b-navbar toggleable="sm" type="dark" variant="success">
|
||||
<b-navbar-brand to="/">
|
||||
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
|
||||
</b-navbar-brand>
|
||||
|
||||
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||
|
||||
<b-collapse id="nav-collapse" is-nav>
|
||||
<b-navbar-nav>
|
||||
<b-nav-item to="/">Übersicht |</b-nav-item>
|
||||
<b-nav-item to="/user">Usersuche |</b-nav-item>
|
||||
<b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
|
||||
<b-nav-item
|
||||
v-show="$store.state.openCreations > 0"
|
||||
class="h5 bg-danger"
|
||||
to="/creation-confirm"
|
||||
>
|
||||
| {{ $store.state.openCreations }} offene Schöpfungen
|
||||
</b-nav-item>
|
||||
<b-nav-item @click="wallet">Wallet</b-nav-item>
|
||||
<b-nav-item @click="logout">Logout</b-nav-item>
|
||||
<!-- <b-nav-item v-show="open < 1" to="/creation-confirm">| keine offene Schöpfungen</b-nav-item> -->
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CONFIG from '../config'
|
||||
|
||||
export default {
|
||||
name: 'navbar',
|
||||
methods: {
|
||||
logout() {
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push('/logout')
|
||||
},
|
||||
wallet() {
|
||||
window.location = CONFIG.WALLET_AUTH_URL.replace('$1', this.$store.state.token)
|
||||
this.$store.dispatch('logout') // logout without redirect
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.navbar-brand-img {
|
||||
height: 2rem;
|
||||
}
|
||||
</style>
|
||||
29
admin/src/components/UserTable.spec.js
Normal file
29
admin/src/components/UserTable.spec.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserTable from './UserTable.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('UserTable', () => {
|
||||
let wrapper
|
||||
|
||||
const propsData = {
|
||||
type: 'Type',
|
||||
itemsUser: [],
|
||||
fieldsTable: [],
|
||||
creation: [],
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(UserTable, { localVue, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-user-table', () => {
|
||||
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
258
admin/src/components/UserTable.vue
Normal file
258
admin/src/components/UserTable.vue
Normal file
@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="component-user-table">
|
||||
<div v-show="overlay" id="overlay" class="">
|
||||
<b-jumbotron class="bg-light p-4">
|
||||
<template #header>{{ overlayText.header }}</template>
|
||||
|
||||
<template #lead>
|
||||
{{ overlayText.text1 }}
|
||||
</template>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<p>
|
||||
{{ overlayText.text2 }}
|
||||
</p>
|
||||
|
||||
<b-button size="md" variant="danger" class="m-3" @click="overlayCancel">
|
||||
{{ overlayText.button_cancel }}
|
||||
</b-button>
|
||||
<b-button
|
||||
size="md"
|
||||
variant="success"
|
||||
class="m-3 text-right"
|
||||
@click="overlayOK(overlayBookmarkType, overlayItem)"
|
||||
>
|
||||
{{ overlayText.button_ok }}
|
||||
</b-button>
|
||||
</b-jumbotron>
|
||||
</div>
|
||||
<b-table-lite
|
||||
:items="itemsUser"
|
||||
:fields="fieldsTable"
|
||||
:filter="criteria"
|
||||
caption-top
|
||||
striped
|
||||
hover
|
||||
stacked="md"
|
||||
>
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button
|
||||
variant="info"
|
||||
size="md"
|
||||
@click="editCreationUserTable(row, row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon v-if="row.detailsShowing" icon="x" aria-label="Help"></b-icon>
|
||||
<b-icon v-else icon="pencil-square" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(show_details)="row">
|
||||
<b-button variant="info" size="md" @click="row.toggleDetails" class="mr-2">
|
||||
<b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
|
||||
<b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #row-details="row">
|
||||
<b-card class="shadow-lg p-3 mb-5 bg-white rounded">
|
||||
<b-row class="mb-2">
|
||||
<b-col></b-col>
|
||||
</b-row>
|
||||
|
||||
<creation-formular
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:creationUserData="creationData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
|
||||
<b-button size="sm" @click="row.toggleDetails">
|
||||
<b-icon
|
||||
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
Details verbergen von {{ row.item.firstName }} {{ row.item.lastName }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
variant="warning"
|
||||
v-show="type === 'UserListSearch'"
|
||||
size="md"
|
||||
@click="bookmarkPush(row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="plus" variant="success"></b-icon>
|
||||
</b-button>
|
||||
<b-button
|
||||
variant="danger"
|
||||
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
|
||||
size="md"
|
||||
@click="overlayShow('remove', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(confirm)="row">
|
||||
<b-button
|
||||
variant="success"
|
||||
v-show="type === 'PageCreationConfirm'"
|
||||
size="md"
|
||||
@click="overlayShow('confirm', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table-lite>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CreationFormular from '../components/CreationFormular.vue'
|
||||
|
||||
export default {
|
||||
name: 'UserTable',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemsUser: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fieldsTable: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
criteria: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
creation: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CreationFormular,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creationData: {},
|
||||
overlay: false,
|
||||
overlayBookmarkType: '',
|
||||
overlayItem: [],
|
||||
overlayText: [
|
||||
{
|
||||
header: '-',
|
||||
text1: '--',
|
||||
text2: '---',
|
||||
button_ok: 'OK',
|
||||
button_cancel: 'Cancel',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
overlayShow(bookmarkType, item) {
|
||||
this.overlay = true
|
||||
this.overlayBookmarkType = bookmarkType
|
||||
this.overlayItem = item
|
||||
|
||||
if (bookmarkType === 'remove') {
|
||||
this.overlayText.header = 'Achtung! Schöpfung löschen!'
|
||||
this.overlayText.text1 =
|
||||
'Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.'
|
||||
this.overlayText.text2 = 'Willst du die vorgespeicherte Schöpfung wirklich löschen? '
|
||||
this.overlayText.button_ok = 'Ja, Schöpfung löschen!'
|
||||
this.overlayText.button_cancel = 'Nein, nicht löschen.'
|
||||
}
|
||||
if (bookmarkType === 'confirm') {
|
||||
this.overlayText.header = 'Schöpfung bestätigen!'
|
||||
this.overlayText.text1 =
|
||||
'Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.'
|
||||
this.overlayText.text2 =
|
||||
'Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?'
|
||||
this.overlayText.button_ok = 'Ja, Schöpfung speichern und bestätigen!'
|
||||
this.overlayText.button_cancel = 'Nein, nicht speichern.'
|
||||
}
|
||||
},
|
||||
overlayOK(bookmarkType, item) {
|
||||
if (bookmarkType === 'remove') {
|
||||
this.bookmarkRemove(item)
|
||||
}
|
||||
if (bookmarkType === 'confirm') {
|
||||
this.bookmarkConfirm(item)
|
||||
}
|
||||
this.overlay = false
|
||||
},
|
||||
overlayCancel() {
|
||||
this.overlay = false
|
||||
},
|
||||
bookmarkPush(item) {
|
||||
this.$emit('update-item', item, 'push')
|
||||
},
|
||||
bookmarkRemove(item) {
|
||||
if (this.type === 'UserListMassCreation') {
|
||||
this.$emit('update-item', item, 'remove')
|
||||
}
|
||||
|
||||
if (this.type === 'PageCreationConfirm') {
|
||||
this.$emit('remove-confirm-result', item, 'remove')
|
||||
}
|
||||
},
|
||||
bookmarkConfirm(item) {
|
||||
alert('die schöpfung bestätigen und abschließen')
|
||||
alert(JSON.stringify(item))
|
||||
this.$emit('remove-confirm-result', item, 'remove')
|
||||
},
|
||||
editCreationUserTable(row, rowItem) {
|
||||
alert('editCreationUserTable')
|
||||
if (!row.detailsShowing) {
|
||||
alert('offen edit loslegen')
|
||||
// this.item = rowItem
|
||||
this.creationData = rowItem
|
||||
// alert(this.creationData)
|
||||
}
|
||||
row.toggleDetails()
|
||||
},
|
||||
updateCreationData(data) {
|
||||
this.creationData = {
|
||||
...data,
|
||||
}
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-left: 5%;
|
||||
background-color: rgba(12, 11, 11, 0.781);
|
||||
z-index: 1000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@ -17,8 +17,13 @@ const environment = {
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
|
||||
const server = {
|
||||
const endpoints = {
|
||||
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
|
||||
WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/vue/authenticate?token=$1',
|
||||
}
|
||||
|
||||
const debug = {
|
||||
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false,
|
||||
}
|
||||
|
||||
const options = {}
|
||||
@ -26,8 +31,9 @@ const options = {}
|
||||
const CONFIG = {
|
||||
...version,
|
||||
...environment,
|
||||
...server,
|
||||
...endpoints,
|
||||
...options,
|
||||
...debug,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
|
||||
19
admin/src/graphql/createPendingCreation.js
Normal file
19
admin/src/graphql/createPendingCreation.js
Normal file
@ -0,0 +1,19 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const createPendingCreation = gql`
|
||||
mutation (
|
||||
$email: String!
|
||||
$amount: Int!
|
||||
$memo: String!
|
||||
$creationDate: String!
|
||||
$moderator: Int!
|
||||
) {
|
||||
createPendingCreation(
|
||||
email: $email
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
creationDate: $creationDate
|
||||
moderator: $moderator
|
||||
)
|
||||
}
|
||||
`
|
||||
12
admin/src/graphql/searchUsers.js
Normal file
12
admin/src/graphql/searchUsers.js
Normal file
@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const searchUsers = gql`
|
||||
query ($searchText: String!) {
|
||||
searchUsers(searchText: $searchText) {
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
creation
|
||||
}
|
||||
}
|
||||
`
|
||||
11
admin/src/graphql/verifyLogin.js
Normal file
11
admin/src/graphql/verifyLogin.js
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const verifyLogin = gql`
|
||||
query {
|
||||
verifyLogin {
|
||||
firstName
|
||||
lastName
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
30
admin/src/i18n.test.js
Normal file
30
admin/src/i18n.test.js
Normal file
@ -0,0 +1,30 @@
|
||||
import i18n from './i18n'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
jest.mock('vue-i18n')
|
||||
|
||||
describe('i18n', () => {
|
||||
it('calls i18n with locale en', () => {
|
||||
expect(VueI18n).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
locale: 'en',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('calls i18n with fallback locale en', () => {
|
||||
expect(VueI18n).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
fallbackLocale: 'en',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has a _t function', () => {
|
||||
expect(i18n).toEqual(
|
||||
expect.objectContaining({
|
||||
_t: expect.anything(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
19
admin/src/layouts/defaultLayout.vue
Normal file
19
admin/src/layouts/defaultLayout.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav-bar class="wrapper-nav" />
|
||||
<router-view class="wrapper p-3"></router-view>
|
||||
<content-footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import ContentFooter from '@/components/ContentFooter.vue'
|
||||
export default {
|
||||
name: 'defaultLayout',
|
||||
components: {
|
||||
NavBar,
|
||||
ContentFooter,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -16,29 +16,34 @@ import VueApollo from 'vue-apollo'
|
||||
|
||||
import CONFIG from './config'
|
||||
|
||||
import { BootstrapVue } from 'bootstrap-vue'
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import moment from 'vue-moment'
|
||||
import Toasted from 'vue-toasted'
|
||||
|
||||
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
|
||||
|
||||
const authLink = new ApolloLink((operation, forward) => {
|
||||
const token = store.state.token
|
||||
|
||||
operation.setContext({
|
||||
headers: {
|
||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||
},
|
||||
})
|
||||
return forward(operation)
|
||||
/* .map((response) => {
|
||||
return forward(operation).map((response) => {
|
||||
if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') {
|
||||
response.errors[0].message = i18n.t('error.session-expired')
|
||||
store.dispatch('logout', null)
|
||||
if (router.currentRoute.path !== '/login') router.push('/login')
|
||||
if (router.currentRoute.path !== '/logout') router.push('/logout')
|
||||
return response
|
||||
}
|
||||
const newToken = operation.getContext().response.headers.get('token')
|
||||
if (newToken) store.commit('token', newToken)
|
||||
return response
|
||||
}) */
|
||||
})
|
||||
})
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
@ -52,9 +57,28 @@ const apolloProvider = new VueApollo({
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
Vue.use(IconsPlugin)
|
||||
|
||||
Vue.use(moment)
|
||||
|
||||
Vue.use(VueApollo)
|
||||
|
||||
Vue.use(Toasted, {
|
||||
position: 'top-center',
|
||||
duration: 5000,
|
||||
fullWidth: true,
|
||||
action: {
|
||||
text: 'x',
|
||||
onClick: (e, toastObject) => {
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
addNavigationGuards(router, store)
|
||||
|
||||
new Vue({
|
||||
moment,
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
|
||||
@ -3,12 +3,17 @@ import './main'
|
||||
import CONFIG from './config'
|
||||
|
||||
import Vue from 'vue'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import Vuex from 'vuex'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import moment from 'vue-moment'
|
||||
|
||||
jest.mock('vue')
|
||||
jest.mock('vue-apollo')
|
||||
jest.mock('vuex')
|
||||
jest.mock('vue-i18n')
|
||||
jest.mock('vue-moment')
|
||||
|
||||
const storeMock = jest.fn()
|
||||
Vuex.Store = storeMock
|
||||
@ -25,6 +30,16 @@ jest.mock('apollo-boost', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('bootstrap-vue', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
BootstrapVue: jest.fn(),
|
||||
IconsPlugin: jest.fn(() => {
|
||||
return { concat: jest.fn() }
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('main', () => {
|
||||
it('calls the HttpLink', () => {
|
||||
expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI })
|
||||
@ -42,6 +57,10 @@ describe('main', () => {
|
||||
expect(InMemoryCache).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls the VueApollo', () => {
|
||||
expect(VueApollo).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls Vue', () => {
|
||||
expect(Vue).toBeCalled()
|
||||
})
|
||||
@ -50,6 +69,18 @@ describe('main', () => {
|
||||
expect(VueI18n).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls BootstrapVue', () => {
|
||||
expect(Vue.use).toBeCalledWith(BootstrapVue)
|
||||
})
|
||||
|
||||
it('calls IconsPlugin', () => {
|
||||
expect(Vue.use).toBeCalledWith(IconsPlugin)
|
||||
})
|
||||
|
||||
it('calls Moment', () => {
|
||||
expect(Vue.use).toBeCalledWith(moment)
|
||||
})
|
||||
|
||||
it.skip('creates a store', () => {
|
||||
expect(storeMock).toBeCalled()
|
||||
})
|
||||
|
||||
59
admin/src/pages/Creation.spec.js
Normal file
59
admin/src/pages/Creation.spec.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Creation from './Creation.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
searchUsers: [
|
||||
{
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('Creation', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Creation, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.creation', () => {
|
||||
expect(wrapper.find('div.creation').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('apollo returns error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
143
admin/src/pages/Creation.vue
Normal file
143
admin/src/pages/Creation.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="creation">
|
||||
<b-row>
|
||||
<b-col cols="12" lg="5">
|
||||
<label>Usersuche</label>
|
||||
<b-input
|
||||
type="text"
|
||||
v-model="criteria"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
placeholder="User suche"
|
||||
></b-input>
|
||||
<user-table
|
||||
v-if="itemsList.length > 0"
|
||||
type="UserListSearch"
|
||||
:itemsUser="itemsList"
|
||||
:fieldsTable="Searchfields"
|
||||
:criteria="criteria"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
|
||||
<user-table
|
||||
v-if="massCreation.length > 0"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
type="UserListMassCreation"
|
||||
:itemsUser="massCreation"
|
||||
:fieldsTable="fields"
|
||||
:criteria="null"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
|
||||
<creation-formular
|
||||
v-if="massCreation.length > 0"
|
||||
type="massCreation"
|
||||
:creation="creation"
|
||||
:itemsMassCreation="massCreation"
|
||||
@update-radio-selected="updateRadioSelected"
|
||||
@remove-all-bookmark="removeAllBookmark"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CreationFormular from '../components/CreationFormular.vue'
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
import { searchUsers } from '../graphql/searchUsers'
|
||||
|
||||
export default {
|
||||
name: 'Creation',
|
||||
components: {
|
||||
CreationFormular,
|
||||
UserTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
Searchfields: [
|
||||
{ key: 'bookmark', label: 'merken' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'creation', label: 'Creation' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
],
|
||||
fields: [
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'creation', label: 'Creation' },
|
||||
{ key: 'bookmark', label: 'löschen' },
|
||||
],
|
||||
itemsList: [],
|
||||
massCreation: [],
|
||||
radioSelectedMass: '',
|
||||
criteria: '',
|
||||
creation: [null, null, null],
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.getUsers()
|
||||
},
|
||||
methods: {
|
||||
async getUsers() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: searchUsers,
|
||||
variables: {
|
||||
searchText: this.criteria,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.itemsList = result.data.searchUsers.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
showDetails: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
},
|
||||
updateItem(e, event) {
|
||||
let index = 0
|
||||
let findArr = {}
|
||||
|
||||
switch (event) {
|
||||
case 'push':
|
||||
findArr = this.itemsList.find((arr) => arr.id === e.id)
|
||||
index = this.itemsList.indexOf(findArr)
|
||||
this.itemsList.splice(index, 1)
|
||||
this.massCreation.push(e)
|
||||
break
|
||||
case 'remove':
|
||||
findArr = this.massCreation.find((arr) => arr.id === e.id)
|
||||
index = this.massCreation.indexOf(findArr)
|
||||
this.massCreation.splice(index, 1)
|
||||
this.itemsList.push(e)
|
||||
break
|
||||
default:
|
||||
throw new Error(event)
|
||||
}
|
||||
},
|
||||
|
||||
updateRadioSelected(obj) {
|
||||
this.radioSelectedMass = obj[0]
|
||||
},
|
||||
|
||||
removeAllBookmark() {
|
||||
alert('remove all bookmarks')
|
||||
const index = 0
|
||||
let i = 0
|
||||
|
||||
for (i; i < this.massCreation.length; i++) {
|
||||
this.itemsList.push(this.massCreation[i])
|
||||
}
|
||||
this.massCreation.splice(index, this.massCreation.length)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
53
admin/src/pages/CreationConfirm.spec.js
Normal file
53
admin/src/pages/CreationConfirm.spec.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('CreationConfirm', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CreationConfirm, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.creation-confirm', () => {
|
||||
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('store', () => {
|
||||
it('commits resetOpenCreations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
|
||||
})
|
||||
|
||||
it('commits openCreationsPlus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsPlus', 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', 1, 'remove')
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
149
admin/src/pages/CreationConfirm.vue
Normal file
149
admin/src/pages/CreationConfirm.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<small class="bg-danger text-light p-1">
|
||||
Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store
|
||||
hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT!
|
||||
</small>
|
||||
<user-table
|
||||
class="mt-4"
|
||||
type="PageCreationConfirm"
|
||||
:itemsUser="confirmResult"
|
||||
:fieldsTable="fields"
|
||||
@remove-confirm-result="removeConfirmResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
components: {
|
||||
UserTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
fields: [
|
||||
{ key: 'bookmark', label: 'löschen' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Vorname' },
|
||||
{ key: 'lastName', label: 'Nachname' },
|
||||
{
|
||||
key: 'creation_gdd',
|
||||
label: 'Schöpfung',
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
},
|
||||
},
|
||||
{ key: 'text', label: 'Text' },
|
||||
{
|
||||
key: 'creation_date',
|
||||
label: 'Datum',
|
||||
formatter: (value) => {
|
||||
return value.long
|
||||
},
|
||||
},
|
||||
{ key: 'creation_moderator', label: 'Moderator' },
|
||||
{ key: 'edit_creation', label: 'ändern' },
|
||||
{ key: 'confirm', label: 'speichern' },
|
||||
],
|
||||
confirmResult: [
|
||||
{
|
||||
id: 1,
|
||||
email: 'dickerson@web.de',
|
||||
firstName: 'Dickerson',
|
||||
lastName: 'Macdonald',
|
||||
creation: '[450,200,700]',
|
||||
creation_gdd: '1000',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
|
||||
|
||||
creation_date: {
|
||||
short: 'November',
|
||||
long: '22/11/2021',
|
||||
},
|
||||
creation_moderator: 'Manuela Gast',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
email: 'larsen@woob.de',
|
||||
firstName: 'Larsen',
|
||||
lastName: 'Shaw',
|
||||
creation: '[300,200,1000]',
|
||||
creation_gdd: '1000',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
|
||||
|
||||
creation_date: {
|
||||
short: 'November',
|
||||
long: '03/11/2021',
|
||||
},
|
||||
creation_moderator: 'Manuela Gast',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
email: 'geneva@tete.de',
|
||||
firstName: 'Geneva',
|
||||
lastName: 'Wilson',
|
||||
creation: '[350,200,900]',
|
||||
creation_gdd: '1000',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam',
|
||||
creation_date: {
|
||||
short: 'September',
|
||||
long: '27/09/2021',
|
||||
},
|
||||
creation_moderator: 'Manuela Gast',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
email: 'viewrter@asdfvb.com',
|
||||
firstName: 'Soledare',
|
||||
lastName: 'Takker',
|
||||
creation: '[100,400,800]',
|
||||
creation_gdd: '500',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo ',
|
||||
creation_date: {
|
||||
short: 'Oktober',
|
||||
long: '12/10/2021',
|
||||
},
|
||||
creation_moderator: 'Evelyn Roller',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
email: 'dickerson@web.de',
|
||||
firstName: 'Dickerson',
|
||||
lastName: 'Macdonald',
|
||||
creation: '[100,400,800]',
|
||||
creation_gdd: '200',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At',
|
||||
creation_date: {
|
||||
short: 'September',
|
||||
long: '05/09/2021',
|
||||
},
|
||||
creation_moderator: 'Manuela Gast',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
removeConfirmResult(e, event) {
|
||||
if (event === 'remove') {
|
||||
let index = 0
|
||||
let findArr = {}
|
||||
|
||||
findArr = this.confirmResult.find((arr) => arr.id === e.id)
|
||||
|
||||
index = this.confirmResult.indexOf(findArr)
|
||||
|
||||
this.confirmResult.splice(index, 1)
|
||||
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.$store.commit('openCreationsPlus', Object.keys(this.confirmResult).length)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
82
admin/src/pages/Overview.vue
Normal file
82
admin/src/pages/Overview.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card
|
||||
v-show="$store.state.openCreations > 0"
|
||||
border-variant="primary"
|
||||
header="offene Schöpfungen"
|
||||
header-bg-variant="danger"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
>
|
||||
<b-card-text>
|
||||
<b-link to="creation-confirm">
|
||||
<h1>{{ $store.state.openCreations }}</h1>
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<b-card
|
||||
v-show="$store.state.openCreations < 1"
|
||||
border-variant="success"
|
||||
header="keine offene Schöpfungen"
|
||||
header-bg-variant="success"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
>
|
||||
<b-card-text>
|
||||
<b-link to="creation-confirm">
|
||||
<h1>{{ $store.state.openCreations }}</h1>
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<br />
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-card border-variant="info" header="offene Registrierung" align="center">
|
||||
<b-card-text>Unbestätigte E-mail Registrierung</b-card-text>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-card border-variant="info" header="geschöpfte Stunden" align="center">
|
||||
<b-card-text>Wievile Stunden können noch von Mitgliedern geschöpft werden?</b-card-text>
|
||||
</b-card>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-card border-variant="info" header="Gemeinschafts Konto" align="center">
|
||||
<b-card-text>
|
||||
Für jedes Mitglied kann für das Gemeinschaftskonto geschöpft werden. Pro Monat 1000 x
|
||||
Mitglieder
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr />
|
||||
<br />
|
||||
<b-list-group>
|
||||
<b-list-group-item class="bg-secondary text-light" href="user">
|
||||
zur Usersuche
|
||||
</b-list-group-item>
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
Mitglieder
|
||||
<b-badge class="bg-success" pill>14</b-badge>
|
||||
</b-list-group-item>
|
||||
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
aktive Mitglieder
|
||||
<b-badge class="bg-primary" pill>12</b-badge>
|
||||
</b-list-group-item>
|
||||
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
nicht bestätigte Mitglieder
|
||||
<b-badge class="bg-warning text-dark" pill>2</b-badge>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
<b-button @click="$store.commit('resetOpenCreations')">
|
||||
lösche alle offenen Test Schöpfungen
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'overview',
|
||||
}
|
||||
</script>
|
||||
59
admin/src/pages/UserSearch.spec.js
Normal file
59
admin/src/pages/UserSearch.spec.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserSearch from './UserSearch.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
searchUsers: [
|
||||
{
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('UserSearch', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(UserSearch, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.user-search', () => {
|
||||
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('apollo returns error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'Ouch',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
76
admin/src/pages/UserSearch.vue
Normal file
76
admin/src/pages/UserSearch.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="user-search">
|
||||
<label>Usersuche</label>
|
||||
<b-input
|
||||
type="text"
|
||||
v-model="criteria"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
placeholder="User suche"
|
||||
@input="getUsers"
|
||||
></b-input>
|
||||
<user-table
|
||||
type="PageUserSearch"
|
||||
:itemsUser="searchResult"
|
||||
:fieldsTable="fields"
|
||||
:criteria="criteria"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
import { searchUsers } from '../graphql/searchUsers'
|
||||
|
||||
export default {
|
||||
name: 'UserSearch',
|
||||
components: {
|
||||
UserTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
fields: [
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{
|
||||
key: 'creation',
|
||||
label: 'Creation',
|
||||
formatter: (value, key, item) => {
|
||||
return String(value)
|
||||
},
|
||||
},
|
||||
{ key: 'show_details', label: 'Details' },
|
||||
],
|
||||
searchResult: [],
|
||||
massCreation: [],
|
||||
criteria: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getUsers() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: searchUsers,
|
||||
variables: {
|
||||
searchText: this.criteria,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.searchResult = result.data.searchUsers.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
// showDetails: true,
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getUsers()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,7 +1,25 @@
|
||||
import CONFIG from '../config'
|
||||
|
||||
const addNavigationGuards = (router, store) => {
|
||||
// store token on `authenticate`
|
||||
router.beforeEach((to, from, next) => {
|
||||
// handle authentication
|
||||
if (to.meta.requiresAuth && !store.state.token) {
|
||||
if (to.path === '/authenticate' && to.query && to.query.token) {
|
||||
// TODO verify user to get user data
|
||||
store.commit('token', to.query.token)
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
// protect all routes but `not-found`
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (
|
||||
!CONFIG.DEBUG_DISABLE_AUTH && // we did not disabled the auth module for debug purposes
|
||||
!store.state.token && // we do not have a token
|
||||
to.path !== '/not-found' && // we are not on `not-found`
|
||||
to.path !== '/logout' // we are not on `logout`
|
||||
) {
|
||||
next({ path: '/not-found' })
|
||||
} else {
|
||||
next()
|
||||
|
||||
64
admin/src/router/guards.test.js
Normal file
64
admin/src/router/guards.test.js
Normal file
@ -0,0 +1,64 @@
|
||||
import addNavigationGuards from './guards'
|
||||
import router from './router'
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const store = {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
token: null,
|
||||
},
|
||||
}
|
||||
|
||||
addNavigationGuards(router, store)
|
||||
|
||||
describe('navigation guards', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('authenticate', () => {
|
||||
const navGuard = router.beforeHooks[0]
|
||||
const next = jest.fn()
|
||||
|
||||
describe('with valid token', () => {
|
||||
it('commits the token to the store', async () => {
|
||||
navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
|
||||
expect(storeCommitMock).toBeCalledWith('token', 'valid-token')
|
||||
})
|
||||
|
||||
it('redirects to /', async () => {
|
||||
navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
|
||||
expect(next).toBeCalledWith({ path: '/' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('without valid token', () => {
|
||||
it('does not commit the token to the store', async () => {
|
||||
navGuard({ path: '/authenticate' }, {}, next)
|
||||
expect(storeCommitMock).not.toBeCalledWith()
|
||||
})
|
||||
|
||||
it('calls next withou arguments', async () => {
|
||||
navGuard({ path: '/authenticate' }, {}, next)
|
||||
expect(next).toBeCalledWith()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('protect all routes', () => {
|
||||
const navGuard = router.beforeHooks[1]
|
||||
const next = jest.fn()
|
||||
|
||||
it('redirects no not found with no token in store ', () => {
|
||||
navGuard({ path: '/' }, {}, next)
|
||||
expect(next).toBeCalledWith({ path: '/not-found' })
|
||||
})
|
||||
|
||||
it('does not redirect when token in store', () => {
|
||||
store.state.token = 'valid token'
|
||||
navGuard({ path: '/' }, {}, next)
|
||||
expect(next).toBeCalledWith()
|
||||
})
|
||||
})
|
||||
})
|
||||
92
admin/src/router/router.test.js
Normal file
92
admin/src/router/router.test.js
Normal file
@ -0,0 +1,92 @@
|
||||
import router from './router'
|
||||
|
||||
describe('router', () => {
|
||||
describe('options', () => {
|
||||
const { options } = router
|
||||
const { scrollBehavior, routes } = options
|
||||
|
||||
it('has "/admin" as base', () => {
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
base: '/admin',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has "active" as linkActiveClass', () => {
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
linkActiveClass: 'active',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has "history" as mode', () => {
|
||||
expect(options).toEqual(
|
||||
expect.objectContaining({
|
||||
mode: 'history',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('scroll behavior', () => {
|
||||
it('returns save position when given', () => {
|
||||
expect(scrollBehavior({}, {}, 'given')).toBe('given')
|
||||
})
|
||||
|
||||
it('returns selector when hash is given', () => {
|
||||
expect(scrollBehavior({ hash: '#to' }, {})).toEqual({ selector: '#to' })
|
||||
})
|
||||
|
||||
it('returns top left coordinates as default', () => {
|
||||
expect(scrollBehavior({}, {})).toEqual({ x: 0, y: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('routes', () => {
|
||||
it('has seven routes defined', () => {
|
||||
expect(routes).toHaveLength(7)
|
||||
})
|
||||
|
||||
it('has "/overview" as default', async () => {
|
||||
const component = await routes.find((r) => r.path === '/').component()
|
||||
expect(component.default.name).toBe('overview')
|
||||
})
|
||||
|
||||
describe('logout', () => {
|
||||
it('loads the "NotFoundPage" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/logout').component()
|
||||
expect(component.default.name).toBe('not-found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user', () => {
|
||||
it('loads the "UserSearch" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/user').component()
|
||||
expect(component.default.name).toBe('UserSearch')
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation', () => {
|
||||
it('loads the "Creation" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/creation').component()
|
||||
expect(component.default.name).toBe('Creation')
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation-confirm', () => {
|
||||
it('loads the "CreationConfirm" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/creation-confirm').component()
|
||||
expect(component.default.name).toBe('CreationConfirm')
|
||||
})
|
||||
})
|
||||
|
||||
describe('not found page', () => {
|
||||
it('renders the "NotFound" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '*').component()
|
||||
expect(component.default.name).toEqual('not-found')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,15 +1,32 @@
|
||||
import NotFound from '@/components/NotFoundPage.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
/*
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
*/
|
||||
path: '/authenticate',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/pages/Overview.vue'),
|
||||
},
|
||||
{
|
||||
// TODO: Implement a "You are logged out"-Page
|
||||
path: '/logout',
|
||||
component: () => import('@/components/NotFoundPage.vue'),
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: () => import('@/pages/UserSearch.vue'),
|
||||
},
|
||||
{
|
||||
path: '/creation',
|
||||
component: () => import('@/pages/Creation.vue'),
|
||||
},
|
||||
{
|
||||
path: '/creation-confirm',
|
||||
component: () => import('@/pages/CreationConfirm.vue'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: () => import('@/components/NotFoundPage.vue'),
|
||||
},
|
||||
{ path: '*', component: NotFound },
|
||||
]
|
||||
|
||||
export default routes
|
||||
|
||||
@ -1,19 +1,49 @@
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
import CONFIG from '../config'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export const mutations = {
|
||||
openCreationsPlus: (state, i) => {
|
||||
state.openCreations += i
|
||||
},
|
||||
openCreationsMinus: (state, i) => {
|
||||
state.openCreations -= i
|
||||
},
|
||||
resetOpenCreations: (state) => {
|
||||
state.openCreations = 0
|
||||
},
|
||||
token: (state, token) => {
|
||||
state.token = token
|
||||
},
|
||||
moderator: (state, moderator) => {
|
||||
state.moderator = moderator
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
logout: ({ commit, state }) => {
|
||||
commit('token', null)
|
||||
window.localStorage.clear()
|
||||
},
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({
|
||||
mutations,
|
||||
plugins: [
|
||||
createPersistedState({
|
||||
storage: window.localStorage,
|
||||
}),
|
||||
],
|
||||
state: {
|
||||
token: 'some-token',
|
||||
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
|
||||
moderator: { name: 'Dertest Moderator', id: 0 },
|
||||
openCreations: 0,
|
||||
},
|
||||
// Syncronous mutation of the state
|
||||
mutations,
|
||||
actions,
|
||||
})
|
||||
|
||||
export default store
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { mutations } from './store'
|
||||
import store, { mutations, actions } from './store'
|
||||
|
||||
const { token } = mutations
|
||||
const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations
|
||||
const { logout } = actions
|
||||
|
||||
const CONFIG = {
|
||||
DEBUG_DISABLE_AUTH: true,
|
||||
}
|
||||
|
||||
describe('Vuex store', () => {
|
||||
describe('mutations', () => {
|
||||
@ -11,5 +16,68 @@ describe('Vuex store', () => {
|
||||
expect(state.token).toEqual('1234')
|
||||
})
|
||||
})
|
||||
|
||||
describe('openCreationsPlus', () => {
|
||||
it('increases the open creations by a given number', () => {
|
||||
const state = { openCreations: 0 }
|
||||
openCreationsPlus(state, 12)
|
||||
expect(state.openCreations).toEqual(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openCreationsMinus', () => {
|
||||
it('decreases the open creations by a given number', () => {
|
||||
const state = { openCreations: 12 }
|
||||
openCreationsMinus(state, 2)
|
||||
expect(state.openCreations).toEqual(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetOpenCreations', () => {
|
||||
it('sets the open creations to 0', () => {
|
||||
const state = { openCreations: 24 }
|
||||
resetOpenCreations(state)
|
||||
expect(state.openCreations).toEqual(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
describe('logout', () => {
|
||||
const windowStorageMock = jest.fn()
|
||||
const commit = jest.fn()
|
||||
const state = {}
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
window.localStorage.clear = windowStorageMock
|
||||
})
|
||||
|
||||
it('deletes the token in store', () => {
|
||||
logout({ commit, state })
|
||||
expect(commit).toBeCalledWith('token', null)
|
||||
})
|
||||
|
||||
it.skip('clears the window local storage', () => {
|
||||
expect(windowStorageMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('state', () => {
|
||||
describe('authentication enabled', () => {
|
||||
it('has no token', () => {
|
||||
expect(store.state.token).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authentication enabled', () => {
|
||||
beforeEach(() => {
|
||||
CONFIG.DEBUG_DISABLE_AUTH = false
|
||||
})
|
||||
|
||||
it.skip('has a token', () => {
|
||||
expect(store.state.token).toBe('validToken')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createLocalVue } from '@vue/test-utils'
|
||||
import Vue from 'vue'
|
||||
import { BootstrapVue } from 'bootstrap-vue'
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
|
||||
// without this async calls are not working
|
||||
import 'regenerator-runtime'
|
||||
@ -8,6 +8,7 @@ import 'regenerator-runtime'
|
||||
global.localVue = createLocalVue()
|
||||
|
||||
global.localVue.use(BootstrapVue)
|
||||
global.localVue.use(IconsPlugin)
|
||||
|
||||
// throw errors for vue warnings to force the programmers to take care about warnings
|
||||
Vue.config.warnHandler = (w) => {
|
||||
|
||||
@ -6424,6 +6424,11 @@ har-validator@~5.1.3:
|
||||
ajv "^6.12.3"
|
||||
har-schema "^2.0.0"
|
||||
|
||||
harmony-reflect@^1.4.6:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710"
|
||||
integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==
|
||||
|
||||
has-ansi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||
@ -6776,6 +6781,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||
dependencies:
|
||||
postcss "^7.0.14"
|
||||
|
||||
identity-obj-proxy@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
|
||||
integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=
|
||||
dependencies:
|
||||
harmony-reflect "^1.4.6"
|
||||
|
||||
ieee754@^1.1.4:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
@ -9020,6 +9032,11 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
moment@^2.19.2, moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
@ -11163,6 +11180,11 @@ shellwords@^0.1.1:
|
||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||
|
||||
shvl@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1"
|
||||
integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==
|
||||
|
||||
side-channel@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||
@ -12469,6 +12491,13 @@ vue-loader@^15.9.2:
|
||||
vue-hot-reload-api "^2.3.0"
|
||||
vue-style-loader "^4.1.0"
|
||||
|
||||
vue-moment@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-moment/-/vue-moment-4.1.0.tgz#092a8ff723a96c6f85a0a8e23ad30f0bf320f3b0"
|
||||
integrity sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw==
|
||||
dependencies:
|
||||
moment "^2.19.2"
|
||||
|
||||
vue-router@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999"
|
||||
@ -12495,11 +12524,24 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-toasted@^1.1.28:
|
||||
version "1.1.28"
|
||||
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
|
||||
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
|
||||
|
||||
vue@^2.6.11:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
||||
|
||||
vuex-persistedstate@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53"
|
||||
integrity sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==
|
||||
dependencies:
|
||||
deepmerge "^4.2.2"
|
||||
shvl "^2.0.3"
|
||||
|
||||
vuex@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||
|
||||
@ -30,4 +30,6 @@ COMMUNITY_URL=
|
||||
COMMUNITY_REGISTER_URL=
|
||||
COMMUNITY_DESCRIPTION=
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
@ -20,6 +20,7 @@
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"apollo-server-testing": "^2.25.2",
|
||||
"axios": "^0.21.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
@ -28,6 +29,7 @@
|
||||
"jest": "^27.2.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"nodemailer": "^6.6.5",
|
||||
"random-bigint": "^0.0.1",
|
||||
|
||||
5
backend/src/auth/CustomJwtPayload.ts
Normal file
5
backend/src/auth/CustomJwtPayload.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { JwtPayload } from 'jsonwebtoken'
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
pubKey: Buffer
|
||||
}
|
||||
13
backend/src/auth/INALIENABLE_RIGHTS.ts
Normal file
13
backend/src/auth/INALIENABLE_RIGHTS.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { RIGHTS } from './RIGHTS'
|
||||
|
||||
export const INALIENABLE_RIGHTS = [
|
||||
RIGHTS.LOGIN,
|
||||
RIGHTS.GET_COMMUNITY_INFO,
|
||||
RIGHTS.COMMUNITIES,
|
||||
RIGHTS.LOGIN_VIA_EMAIL_VERIFICATION_CODE,
|
||||
RIGHTS.CREATE_USER,
|
||||
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
|
||||
RIGHTS.RESET_PASSWORD,
|
||||
RIGHTS.CHECK_USERNAME,
|
||||
RIGHTS.CHECK_EMAIL,
|
||||
]
|
||||
19
backend/src/auth/JWT.ts
Normal file
19
backend/src/auth/JWT.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
import { CustomJwtPayload } from './CustomJwtPayload'
|
||||
|
||||
export const decode = (token: string): CustomJwtPayload | null => {
|
||||
if (!token) throw new Error('401 Unauthorized')
|
||||
try {
|
||||
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const encode = (pubKey: Buffer): string => {
|
||||
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
|
||||
expiresIn: CONFIG.JWT_EXPIRES_IN,
|
||||
})
|
||||
return token
|
||||
}
|
||||
26
backend/src/auth/RIGHTS.ts
Normal file
26
backend/src/auth/RIGHTS.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export enum RIGHTS {
|
||||
LOGIN = 'LOGIN',
|
||||
VERIFY_LOGIN = 'VERIFY_LOGIN',
|
||||
BALANCE = 'BALANCE',
|
||||
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
|
||||
COMMUNITIES = 'COMMUNITIES',
|
||||
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
|
||||
EXIST_PID = 'EXIST_PID',
|
||||
GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER',
|
||||
GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP',
|
||||
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
|
||||
SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER',
|
||||
TRANSACTION_LIST = 'TRANSACTION_LIST',
|
||||
SEND_COINS = 'SEND_COINS',
|
||||
LOGIN_VIA_EMAIL_VERIFICATION_CODE = 'LOGIN_VIA_EMAIL_VERIFICATION_CODE',
|
||||
LOGOUT = 'LOGOUT',
|
||||
CREATE_USER = 'CREATE_USER',
|
||||
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
|
||||
RESET_PASSWORD = 'RESET_PASSWORD',
|
||||
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
|
||||
CHECK_USERNAME = 'CHECK_USERNAME',
|
||||
CHECK_EMAIL = 'CHECK_EMAIL',
|
||||
HAS_ELOPAGE = 'HAS_ELOPAGE',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
}
|
||||
25
backend/src/auth/ROLES.ts
Normal file
25
backend/src/auth/ROLES.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
|
||||
import { RIGHTS } from './RIGHTS'
|
||||
import { Role } from './Role'
|
||||
|
||||
export const ROLE_UNAUTHORIZED = new Role('unauthorized', INALIENABLE_RIGHTS)
|
||||
export const ROLE_USER = new Role('user', [
|
||||
...INALIENABLE_RIGHTS,
|
||||
RIGHTS.VERIFY_LOGIN,
|
||||
RIGHTS.BALANCE,
|
||||
RIGHTS.LIST_GDT_ENTRIES,
|
||||
RIGHTS.EXIST_PID,
|
||||
RIGHTS.GET_KLICKTIPP_USER,
|
||||
RIGHTS.GET_KLICKTIPP_TAG_MAP,
|
||||
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
|
||||
RIGHTS.SUBSCRIBE_NEWSLETTER,
|
||||
RIGHTS.TRANSACTION_LIST,
|
||||
RIGHTS.SEND_COINS,
|
||||
RIGHTS.LOGOUT,
|
||||
RIGHTS.UPDATE_USER_INFOS,
|
||||
RIGHTS.HAS_ELOPAGE,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
// TODO from database
|
||||
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN]
|
||||
15
backend/src/auth/Role.ts
Normal file
15
backend/src/auth/Role.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { RIGHTS } from './RIGHTS'
|
||||
|
||||
export class Role {
|
||||
id: string
|
||||
rights: RIGHTS[]
|
||||
|
||||
constructor(id: string, rights: RIGHTS[]) {
|
||||
this.id = id
|
||||
this.rights = rights
|
||||
}
|
||||
|
||||
hasRight = (right: RIGHTS): boolean => {
|
||||
return this.rights.includes(right)
|
||||
}
|
||||
}
|
||||
@ -55,9 +55,21 @@ const email = {
|
||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
|
||||
}
|
||||
|
||||
const webhook = {
|
||||
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||
}
|
||||
|
||||
// This is needed by graphql-directive-auth
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email, ...loginServer }
|
||||
const CONFIG = {
|
||||
...server,
|
||||
...database,
|
||||
...klicktipp,
|
||||
...community,
|
||||
...email,
|
||||
...loginServer,
|
||||
...webhook,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
|
||||
19
backend/src/graphql/arg/CreatePendingCreationArgs.ts
Normal file
19
backend/src/graphql/arg/CreatePendingCreationArgs.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CreatePendingCreationArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Int)
|
||||
amount: number
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
creationDate: string
|
||||
|
||||
@Field(() => Int)
|
||||
moderator: number
|
||||
}
|
||||
@ -15,7 +15,7 @@ export default class CreateUserArgs {
|
||||
password: string
|
||||
|
||||
@Field(() => String)
|
||||
language: string
|
||||
language?: string // Will default to DEFAULT_LANGUAGE
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId: number
|
||||
|
||||
@ -2,19 +2,44 @@
|
||||
|
||||
import { AuthChecker } from 'type-graphql'
|
||||
|
||||
import decode from '../../jwt/decode'
|
||||
import encode from '../../jwt/encode'
|
||||
import { decode, encode } from '../../auth/JWT'
|
||||
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '../../auth/ROLES'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
|
||||
const isAuthorized: AuthChecker<any> = async (
|
||||
{ /* root, args, */ context /*, info */ } /*, roles */,
|
||||
) => {
|
||||
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
||||
|
||||
// Do we have a token?
|
||||
if (context.token) {
|
||||
const decoded = decode(context.token)
|
||||
if (!decoded) {
|
||||
// we always throw on an invalid token
|
||||
throw new Error('403.13 - Client certificate revoked')
|
||||
}
|
||||
// Set context pubKey
|
||||
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
|
||||
// set new header token
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
const userRepository = await getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const serverUserRepository = await getCustomRepository(ServerUserRepository)
|
||||
const countServerUsers = await serverUserRepository.count({ email: user.email })
|
||||
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
|
||||
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
|
||||
return true
|
||||
}
|
||||
throw new Error('401 Unauthorized')
|
||||
|
||||
// check for correct rights
|
||||
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
|
||||
if (missingRights.length !== 0) {
|
||||
throw new Error('401 Unauthorized')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default isAuthorized
|
||||
|
||||
0
backend/src/graphql/model/CreatePendingCreation.ts
Normal file
0
backend/src/graphql/model/CreatePendingCreation.ts
Normal file
@ -12,6 +12,7 @@ export class User {
|
||||
*/
|
||||
constructor(json?: any) {
|
||||
if (json) {
|
||||
this.id = json.id
|
||||
this.email = json.email
|
||||
this.firstName = json.first_name
|
||||
this.lastName = json.last_name
|
||||
@ -20,9 +21,13 @@ export class User {
|
||||
this.pubkey = json.public_hex
|
||||
this.language = json.language
|
||||
this.publisherId = json.publisher_id
|
||||
this.isAdmin = json.isAdmin
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@ -48,7 +53,7 @@ export class User {
|
||||
@Field(() => number)
|
||||
created: number
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Field(() =>>> Boolean)
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
@ -71,6 +76,9 @@ export class User {
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId?: number
|
||||
|
||||
@Field(() => Boolean)
|
||||
isAdmin: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
coinanimation: boolean
|
||||
|
||||
|
||||
16
backend/src/graphql/model/UserAdmin.ts
Normal file
16
backend/src/graphql/model/UserAdmin.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class UserAdmin {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => [Number])
|
||||
creation: number[]
|
||||
}
|
||||
176
backend/src/graphql/resolver/AdminResolver.ts
Normal file
176
backend/src/graphql/resolver/AdminResolver.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { Resolver, Query, Arg, Args, Authorized, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, Raw } from 'typeorm'
|
||||
import { UserAdmin } from '../model/UserAdmin'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { TransactionCreationRepository } from '../../typeorm/repository/TransactionCreation'
|
||||
import { PendingCreationRepository } from '../../typeorm/repository/PendingCreation'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
|
||||
import moment from 'moment'
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@Authorized([RIGHTS.SEARCH_USERS])
|
||||
@Query(() => [UserAdmin])
|
||||
async searchUsers(@Arg('searchText') searchText: string): Promise<UserAdmin[]> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUsers = await loginUserRepository.findBySearchCriteria(searchText)
|
||||
const users = await Promise.all(
|
||||
loginUsers.map(async (loginUser) => {
|
||||
const user = new UserAdmin()
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
user.email = loginUser.email
|
||||
user.creation = await getUserCreations(loginUser.id)
|
||||
return user
|
||||
}),
|
||||
)
|
||||
return users
|
||||
}
|
||||
|
||||
@Mutation(() => [Number])
|
||||
async createPendingCreation(
|
||||
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
|
||||
): Promise<number[]> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findByEmail(email)
|
||||
|
||||
const creations = await getUserCreations(user.id)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
||||
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
|
||||
const loginPendingTaskAdmin = pendingCreationRepository.create()
|
||||
loginPendingTaskAdmin.userId = user.id
|
||||
loginPendingTaskAdmin.amount = BigInt(amount * 10000)
|
||||
loginPendingTaskAdmin.created = new Date()
|
||||
loginPendingTaskAdmin.date = creationDateObj
|
||||
loginPendingTaskAdmin.memo = memo
|
||||
loginPendingTaskAdmin.moderator = moderator
|
||||
|
||||
pendingCreationRepository.save(loginPendingTaskAdmin)
|
||||
}
|
||||
return await getUserCreations(user.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserCreations(id: number): Promise<number[]> {
|
||||
const dateNextMonth = moment().add(1, 'month').format('YYYY-MM') + '-01'
|
||||
const dateMonth = moment().format('YYYY-MM') + '-01'
|
||||
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM') + '-01'
|
||||
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM') + '-01'
|
||||
|
||||
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
|
||||
const createdAmountBeforeLastMonth = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
enddate: dateLastMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const createdAmountLastMonth = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateLastMonth,
|
||||
enddate: dateMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const createdAmountMonth = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateMonth,
|
||||
enddate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
|
||||
const pendingAmountMounth = await pendingCreationRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateMonth,
|
||||
enddate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const pendingAmountLastMounth = await pendingCreationRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateLastMonth,
|
||||
enddate: dateMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const pendingAmountBeforeLastMounth = await pendingCreationRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
enddate: dateLastMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
// COUNT amount from 2 tables
|
||||
const usedCreationBeforeLastMonth =
|
||||
(Number(createdAmountBeforeLastMonth.sum) + Number(pendingAmountBeforeLastMounth.sum)) / 10000
|
||||
const usedCreationLastMonth =
|
||||
(Number(createdAmountLastMonth.sum) + Number(pendingAmountLastMounth.sum)) / 10000
|
||||
const usedCreationMonth =
|
||||
(Number(createdAmountMonth.sum) + Number(pendingAmountMounth.sum)) / 10000
|
||||
return [
|
||||
1000 - usedCreationBeforeLastMonth,
|
||||
1000 - usedCreationLastMonth,
|
||||
1000 - usedCreationMonth,
|
||||
]
|
||||
}
|
||||
|
||||
function isCreationValid(creations: number[], amount: number, creationDate: Date) {
|
||||
const dateMonth = moment().format('YYYY-MM')
|
||||
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM')
|
||||
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM')
|
||||
const creationDateMonth = moment(creationDate).format('YYYY-MM')
|
||||
|
||||
let openCreation
|
||||
switch (creationDateMonth) {
|
||||
case dateMonth:
|
||||
openCreation = creations[2]
|
||||
break
|
||||
case dateLastMonth:
|
||||
openCreation = creations[1]
|
||||
break
|
||||
case dateBeforeLastMonth:
|
||||
openCreation = creations[0]
|
||||
break
|
||||
default:
|
||||
throw new Error('CreationDate is not in last three months')
|
||||
}
|
||||
|
||||
if (openCreation < amount) {
|
||||
throw new Error(`Open creation (${openCreation}) is less than amount (${amount})`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -8,10 +8,11 @@ import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.BALANCE])
|
||||
@Query(() => Balance)
|
||||
async balance(@Ctx() context: any): Promise<Balance> {
|
||||
// load user and balance
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query } from 'type-graphql'
|
||||
import { Resolver, Query, Authorized } from 'type-graphql'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import CONFIG from '../../config'
|
||||
import { Community } from '../model/Community'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@Authorized([RIGHTS.GET_COMMUNITY_INFO])
|
||||
@Query(() => Community)
|
||||
async getCommunityInfo(): Promise<Community> {
|
||||
return new Community({
|
||||
@ -17,6 +19,7 @@ export class CommunityResolver {
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITIES])
|
||||
@Query(() => [Community])
|
||||
async communities(): Promise<Community[]> {
|
||||
if (CONFIG.PRODUCTION)
|
||||
|
||||
@ -9,10 +9,11 @@ import Paginated from '../arg/Paginated'
|
||||
import { apiGet } from '../../apis/HttpRequest'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { Order } from '../enum/Order'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
|
||||
@Resolver()
|
||||
export class GdtResolver {
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.LIST_GDT_ENTRIES])
|
||||
@Query(() => GdtEntryList)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async listGDTEntries(
|
||||
@ -33,7 +34,7 @@ export class GdtResolver {
|
||||
return new GdtEntryList(resultGDT.data)
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.EXIST_PID])
|
||||
@Query(() => Number)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async existPid(@Arg('pid') pid: number): Promise<number> {
|
||||
|
||||
@ -8,29 +8,30 @@ import {
|
||||
unsubscribe,
|
||||
signIn,
|
||||
} from '../../apis/KlicktippController'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import SubscribeNewsletterArgs from '../arg/SubscribeNewsletterArgs'
|
||||
|
||||
@Resolver()
|
||||
export class KlicktippResolver {
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.GET_KLICKTIPP_USER])
|
||||
@Query(() => String)
|
||||
async getKlicktippUser(@Arg('email') email: string): Promise<string> {
|
||||
return await getKlickTippUser(email)
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.GET_KLICKTIPP_TAG_MAP])
|
||||
@Query(() => String)
|
||||
async getKlicktippTagMap(): Promise<string> {
|
||||
return await getKlicktippTagMap()
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER])
|
||||
@Mutation(() => Boolean)
|
||||
async unsubscribeNewsletter(@Arg('email') email: string): Promise<boolean> {
|
||||
return await unsubscribe(email)
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.SUBSCRIBE_NEWSLETTER])
|
||||
@Mutation(() => Boolean)
|
||||
async subscribeNewsletter(
|
||||
@Args() { email, language }: SubscribeNewsletterArgs,
|
||||
|
||||
@ -34,6 +34,7 @@ import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
|
||||
/*
|
||||
# Test
|
||||
@ -465,7 +466,7 @@ async function getPublicKey(email: string): Promise<string | null> {
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.TRANSACTION_LIST])
|
||||
@Query(() => TransactionList)
|
||||
async transactionList(
|
||||
@Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
@ -499,7 +500,7 @@ export class TransactionResolver {
|
||||
return transactions
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@Mutation(() => String)
|
||||
async sendCoins(
|
||||
@Args() { email, amount, memo }: TransactionSendArgs,
|
||||
|
||||
@ -9,7 +9,7 @@ import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
|
||||
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
|
||||
import { User } from '../model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import encode from '../../jwt/encode'
|
||||
import { encode } from '../../auth/JWT'
|
||||
import ChangePasswordArgs from '../arg/ChangePasswordArgs'
|
||||
import CheckUsernameArgs from '../arg/CheckUsernameArgs'
|
||||
import CreateUserArgs from '../arg/CreateUserArgs'
|
||||
@ -30,6 +30,9 @@ import { LoginUserBackup } from '@entity/LoginUserBackup'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendEMail } from '../../util/sendEMail'
|
||||
import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
|
||||
import { ROLE_ADMIN } from '../../auth/ROLES'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
@ -194,6 +197,42 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
||||
|
||||
@Resolver()
|
||||
export class UserResolver {
|
||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||
@Query(() => User)
|
||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||
async verifyLogin(@Ctx() context: any): Promise<User> {
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findByEmail(userEntity.email)
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = userEntity.email
|
||||
user.firstName = userEntity.firstName
|
||||
user.lastName = userEntity.lastName
|
||||
user.username = userEntity.username
|
||||
user.description = loginUser.description
|
||||
user.pubkey = userEntity.pubkey.toString('hex')
|
||||
user.language = loginUser.language
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
|
||||
// coinAnimation
|
||||
const userSettingRepository = getCustomRepository(UserSettingRepository)
|
||||
const coinanimation = await userSettingRepository
|
||||
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
|
||||
.catch((error) => {
|
||||
throw new Error(error)
|
||||
})
|
||||
user.coinanimation = coinanimation
|
||||
|
||||
user.isAdmin = context.role === ROLE_ADMIN
|
||||
return user
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LOGIN])
|
||||
@Query(() => User)
|
||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||
async login(
|
||||
@ -207,6 +246,7 @@ export class UserResolver {
|
||||
const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
|
||||
throw new Error('No user with this credentials')
|
||||
})
|
||||
if (!loginUser.emailChecked) throw new Error('user email not validated')
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||
const loginUserPassword = BigInt(loginUser.password.toString())
|
||||
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
||||
@ -237,6 +277,7 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = email
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
@ -266,6 +307,11 @@ export class UserResolver {
|
||||
})
|
||||
user.coinanimation = coinanimation
|
||||
|
||||
// context.role is not set to the actual role yet on login
|
||||
const serverUserRepository = await getCustomRepository(ServerUserRepository)
|
||||
const countServerUsers = await serverUserRepository.count({ email: user.email })
|
||||
user.isAdmin = countServerUsers > 0
|
||||
|
||||
context.setHeaders.push({
|
||||
key: 'token',
|
||||
value: encode(loginUser.pubKey),
|
||||
@ -274,6 +320,7 @@ export class UserResolver {
|
||||
return user
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LOGIN_VIA_EMAIL_VERIFICATION_CODE])
|
||||
@Query(() => LoginViaVerificationCode)
|
||||
async loginViaEmailVerificationCode(
|
||||
@Arg('optin') optin: string,
|
||||
@ -289,7 +336,7 @@ export class UserResolver {
|
||||
return new LoginViaVerificationCode(result.data)
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.LOGOUT])
|
||||
@Query(() => String)
|
||||
async logout(): Promise<boolean> {
|
||||
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
|
||||
@ -300,6 +347,7 @@ export class UserResolver {
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_USER])
|
||||
@Mutation(() => String)
|
||||
async createUser(
|
||||
@Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs,
|
||||
@ -308,7 +356,7 @@ export class UserResolver {
|
||||
// default int publisher_id = 0;
|
||||
|
||||
// Validate Language (no throw)
|
||||
if (!isLanguage(language)) {
|
||||
if (!language || !isLanguage(language)) {
|
||||
language = DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
@ -440,6 +488,7 @@ export class UserResolver {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
||||
@Query(() => SendPasswordResetEmailResponse)
|
||||
async sendResetPasswordEmail(
|
||||
@Arg('email') email: string,
|
||||
@ -456,6 +505,7 @@ export class UserResolver {
|
||||
return new SendPasswordResetEmailResponse(response.data)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.RESET_PASSWORD])
|
||||
@Mutation(() => String)
|
||||
async resetPassword(
|
||||
@Args()
|
||||
@ -473,7 +523,7 @@ export class UserResolver {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.UPDATE_USER_INFOS])
|
||||
@Mutation(() => Boolean)
|
||||
async updateUserInfos(
|
||||
@Args()
|
||||
@ -582,6 +632,7 @@ export class UserResolver {
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CHECK_USERNAME])
|
||||
@Query(() => Boolean)
|
||||
async checkUsername(@Args() { username }: CheckUsernameArgs): Promise<boolean> {
|
||||
// Username empty?
|
||||
@ -605,6 +656,7 @@ export class UserResolver {
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CHECK_EMAIL])
|
||||
@Query(() => CheckEmailResponse)
|
||||
@UseMiddleware(klicktippRegistrationMiddleware)
|
||||
async checkEmail(@Arg('optin') optin: string): Promise<CheckEmailResponse> {
|
||||
@ -617,7 +669,7 @@ export class UserResolver {
|
||||
return new CheckEmailResponse(result.data)
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Authorized([RIGHTS.HAS_ELOPAGE])
|
||||
@Query(() => Boolean)
|
||||
async hasElopage(@Ctx() context: any): Promise<boolean> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
interface CustomJwtPayload extends JwtPayload {
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
type DecodedJwt = {
|
||||
token: string
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
export default (token: string): DecodedJwt => {
|
||||
if (!token) throw new Error('401 Unauthorized')
|
||||
let pubKey = null
|
||||
try {
|
||||
const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
||||
pubKey = decoded.pubKey
|
||||
return {
|
||||
token,
|
||||
pubKey,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error('403.13 - Client certificate revoked')
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
// Generate an Access Token
|
||||
export default function encode(pubKey: Buffer): string {
|
||||
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
|
||||
expiresIn: CONFIG.JWT_EXPIRES_IN,
|
||||
})
|
||||
return token
|
||||
}
|
||||
@ -6,6 +6,7 @@ import 'module-alias/register'
|
||||
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
|
||||
// database
|
||||
import connection from '../typeorm/connection'
|
||||
@ -22,10 +23,13 @@ import CONFIG from '../config'
|
||||
// graphql
|
||||
import schema from '../graphql/schema'
|
||||
|
||||
// webhooks
|
||||
import { elopageWebhook } from '../webhook/elopage'
|
||||
|
||||
// TODO implement
|
||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||
|
||||
const DB_VERSION = '0004-login_server_data'
|
||||
const DB_VERSION = '0005-admin_tables'
|
||||
|
||||
const createServer = async (context: any = serverContext): Promise<any> => {
|
||||
// open mysql connection
|
||||
@ -50,6 +54,12 @@ const createServer = async (context: any = serverContext): Promise<any> => {
|
||||
// cors
|
||||
app.use(cors)
|
||||
|
||||
// bodyparser
|
||||
app.use(bodyParser.json())
|
||||
|
||||
// Elopage Webhook
|
||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||
|
||||
// Apollo Server
|
||||
const apollo = new ApolloServer({
|
||||
schema: await schema(),
|
||||
|
||||
@ -8,4 +8,17 @@ export class LoginUserRepository extends Repository<LoginUser> {
|
||||
.where('loginUser.email = :email', { email })
|
||||
.getOneOrFail()
|
||||
}
|
||||
|
||||
async findBySearchCriteria(searchCriteria: string): Promise<LoginUser[]> {
|
||||
return await this.createQueryBuilder('user')
|
||||
.where(
|
||||
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
|
||||
{
|
||||
name: `%${searchCriteria}%`,
|
||||
lastName: `%${searchCriteria}%`,
|
||||
email: `%${searchCriteria}%`,
|
||||
},
|
||||
)
|
||||
.getMany()
|
||||
}
|
||||
}
|
||||
|
||||
5
backend/src/typeorm/repository/PendingCreation.ts
Normal file
5
backend/src/typeorm/repository/PendingCreation.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { LoginPendingTasksAdmin } from '@entity/LoginPendingTasksAdmin'
|
||||
|
||||
@EntityRepository(LoginPendingTasksAdmin)
|
||||
export class PendingCreationRepository extends Repository<LoginPendingTasksAdmin> {}
|
||||
5
backend/src/typeorm/repository/ServerUser.ts
Normal file
5
backend/src/typeorm/repository/ServerUser.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { ServerUser } from '@entity/ServerUser'
|
||||
|
||||
@EntityRepository(ServerUser)
|
||||
export class ServerUserRepository extends Repository<ServerUser> {}
|
||||
5
backend/src/typeorm/repository/TransactionCreation.ts
Normal file
5
backend/src/typeorm/repository/TransactionCreation.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { TransactionCreation } from '@entity/TransactionCreation'
|
||||
|
||||
@EntityRepository(TransactionCreation)
|
||||
export class TransactionCreationRepository extends Repository<TransactionCreation> {}
|
||||
154
backend/src/webhook/elopage.ts
Normal file
154
backend/src/webhook/elopage.ts
Normal file
File diff suppressed because one or more lines are too long
@ -1552,7 +1552,7 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
body-parser@1.19.0, body-parser@^1.18.3:
|
||||
body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||
@ -4139,6 +4139,11 @@ module-alias@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
|
||||
integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
|
||||
|
||||
moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
@ -16,7 +16,7 @@ class ServerUsersController extends AppController
|
||||
{
|
||||
parent::initialize();
|
||||
// uncomment in devmode to add new community server admin user, but don't!!! commit it
|
||||
//$this->Auth->allow(['add', 'edit']);
|
||||
// $this->Auth->allow(['add', 'edit']);
|
||||
$this->Auth->deny('index');
|
||||
}
|
||||
|
||||
|
||||
@ -4,4 +4,6 @@ DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
MIGRATIONS_TABLE=migrations
|
||||
MIGRATIONS_DIRECTORY=./migrations/
|
||||
MIGRATIONS_DIRECTORY=./migrations/
|
||||
|
||||
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}
|
||||
|
||||
@ -118,7 +118,7 @@ CMD /bin/sh -c "yarn run up"
|
||||
##################################################################################
|
||||
# PRODUCTION RESET ###############################################################
|
||||
##################################################################################
|
||||
FROM production as production_reset
|
||||
# FROM production as production_reset
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run reset"
|
||||
|
||||
46
database/README.md
Normal file
46
database/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# database
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Upgrade migrations production
|
||||
```
|
||||
yarn up
|
||||
```
|
||||
|
||||
## Upgrade migrations development
|
||||
```
|
||||
yarn dev_up
|
||||
```
|
||||
|
||||
## Downgrade migrations production
|
||||
```
|
||||
yarn down
|
||||
```
|
||||
|
||||
## Downgrade migrations development
|
||||
```
|
||||
yarn dev_down
|
||||
```
|
||||
|
||||
|
||||
## Reset DB
|
||||
```
|
||||
yarn dev_reset
|
||||
```
|
||||
|
||||
## Seed DB
|
||||
```
|
||||
yarn seed
|
||||
```
|
||||
|
||||
## Seeded Users
|
||||
|
||||
| email | password | admin |
|
||||
| peter@lustig.de | `Aa12345_` | `true` |
|
||||
| bibi@bloxberg.de | `Aa12345_` | `false` |
|
||||
| raeuber@hotzenplotz.de | `Aa12345_` | `false` |
|
||||
| bob@baumeister.de | `Aa12345_` | `false` |
|
||||
|
||||
31
database/entity/0001-init_db/ServerUser.ts
Normal file
31
database/entity/0001-init_db/ServerUser.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
|
||||
@Entity('server_users')
|
||||
export class ServerUser extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ length: 50 })
|
||||
username: string
|
||||
|
||||
@Column({ length: 255 })
|
||||
password: string
|
||||
|
||||
@Column({ length: 50, unique: true })
|
||||
email: string
|
||||
|
||||
@Column({ length: 20, default: 'admin' })
|
||||
role: string
|
||||
|
||||
@Column({ default: 0 })
|
||||
activated: number
|
||||
|
||||
@Column({ name: 'last_login', default: null, nullable: true })
|
||||
lastLogin: Date
|
||||
|
||||
@Column({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
created: Date
|
||||
|
||||
@Column({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
modified: Date
|
||||
}
|
||||
@ -3,22 +3,28 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users')
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'index_id', default: 0 })
|
||||
indexId: number
|
||||
|
||||
@Column({ name: 'group_id', default: 0, unsigned: true })
|
||||
groupId: number
|
||||
|
||||
@Column({ type: 'binary', length: 32, name: 'public_key' })
|
||||
pubkey: Buffer
|
||||
|
||||
@Column()
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'first_name' })
|
||||
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
|
||||
firstName: string
|
||||
|
||||
@Column({ name: 'last_name' })
|
||||
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
|
||||
lastName: string
|
||||
|
||||
@Column()
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
username: string
|
||||
|
||||
@Column()
|
||||
|
||||
@ -4,22 +4,28 @@ import { UserSetting } from './UserSetting'
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users')
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'index_id', default: 0 })
|
||||
indexId: number
|
||||
|
||||
@Column({ name: 'group_id', default: 0, unsigned: true })
|
||||
groupId: number
|
||||
|
||||
@Column({ type: 'binary', length: 32, name: 'public_key' })
|
||||
pubkey: Buffer
|
||||
|
||||
@Column()
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'first_name' })
|
||||
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
|
||||
firstName: string
|
||||
|
||||
@Column({ name: 'last_name' })
|
||||
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
|
||||
lastName: string
|
||||
|
||||
@Column()
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
username: string
|
||||
|
||||
@Column()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
|
||||
import { LoginUserBackup } from '../LoginUserBackup'
|
||||
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('login_users')
|
||||
@ -53,4 +54,7 @@ export class LoginUser extends BaseEntity {
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@OneToOne(() => LoginUserBackup, (loginUserBackup) => loginUserBackup.loginUser)
|
||||
loginUserBackup: LoginUserBackup
|
||||
}
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne } from 'typeorm'
|
||||
import { LoginUser } from '../LoginUser'
|
||||
|
||||
@Entity('login_user_backups')
|
||||
export class LoginUserBackup extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'user_id', nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ type: 'text', name: 'passphrase', nullable: false })
|
||||
passphrase: string
|
||||
|
||||
@Column({ name: 'user_id', nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'mnemonic_type', default: -1 })
|
||||
mnemonicType: number
|
||||
|
||||
@OneToOne(() => LoginUser, (loginUser) => loginUser.loginUserBackup, { nullable: false })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
loginUser: LoginUser
|
||||
}
|
||||
|
||||
13
database/entity/0003-login_server_tables/LoginUserRoles.ts
Normal file
13
database/entity/0003-login_server_tables/LoginUserRoles.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
|
||||
@Entity('login_user_roles')
|
||||
export class LoginUserRoles extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'role_id' })
|
||||
roleId: number
|
||||
}
|
||||
25
database/entity/0005-admin_tables/LoginPendingTasksAdmin.ts
Normal file
25
database/entity/0005-admin_tables/LoginPendingTasksAdmin.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity('login_pending_tasks_admin')
|
||||
export class LoginPendingTasksAdmin extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created: Date
|
||||
|
||||
@Column({ type: 'datetime', nullable: false })
|
||||
date: Date
|
||||
|
||||
@Column({ length: 256, nullable: true, default: null })
|
||||
memo: string
|
||||
|
||||
@Column({ type: 'bigint', nullable: false })
|
||||
amount: BigInt
|
||||
|
||||
@Column()
|
||||
moderator: number
|
||||
}
|
||||
1
database/entity/LoginPendingTasksAdmin.ts
Normal file
1
database/entity/LoginPendingTasksAdmin.ts
Normal file
@ -0,0 +1 @@
|
||||
export { LoginPendingTasksAdmin } from './0005-admin_tables/LoginPendingTasksAdmin'
|
||||
1
database/entity/LoginUserRoles.ts
Normal file
1
database/entity/LoginUserRoles.ts
Normal file
@ -0,0 +1 @@
|
||||
export { LoginUserRoles } from './0003-login_server_tables/LoginUserRoles'
|
||||
1
database/entity/ServerUser.ts
Normal file
1
database/entity/ServerUser.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ServerUser } from './0001-init_db/ServerUser'
|
||||
@ -2,26 +2,32 @@ import { Balance } from './Balance'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { LoginEmailOptIn } from './LoginEmailOptIn'
|
||||
import { LoginUser } from './LoginUser'
|
||||
import { LoginUserRoles } from './LoginUserRoles'
|
||||
import { LoginUserBackup } from './LoginUserBackup'
|
||||
import { Migration } from './Migration'
|
||||
import { ServerUser } from './ServerUser'
|
||||
import { Transaction } from './Transaction'
|
||||
import { TransactionCreation } from './TransactionCreation'
|
||||
import { TransactionSendCoin } from './TransactionSendCoin'
|
||||
import { User } from './User'
|
||||
import { UserSetting } from './UserSetting'
|
||||
import { UserTransaction } from './UserTransaction'
|
||||
import { LoginPendingTasksAdmin } from './LoginPendingTasksAdmin'
|
||||
|
||||
export const entities = [
|
||||
Balance,
|
||||
LoginElopageBuys,
|
||||
LoginEmailOptIn,
|
||||
LoginUser,
|
||||
LoginUserRoles,
|
||||
LoginUserBackup,
|
||||
Migration,
|
||||
ServerUser,
|
||||
Transaction,
|
||||
TransactionCreation,
|
||||
TransactionSendCoin,
|
||||
User,
|
||||
UserSetting,
|
||||
UserTransaction,
|
||||
LoginPendingTasksAdmin,
|
||||
]
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
|
||||
29
database/migrations/0005-admin_tables.ts
Normal file
29
database/migrations/0005-admin_tables.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/* MIGRATION FOR ADMIN INTERFACE
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
* the database can be setup already but also may not be.
|
||||
* Therefore you will find all `CREATE TABLE` statements with
|
||||
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
|
||||
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
|
||||
* This ensures compatibility for existing or non-existing
|
||||
* databases.
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE \`login_pending_tasks_admin\` (
|
||||
\`id\` int UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
\`userId\` int UNSIGNED DEFAULT 0,
|
||||
\`created\` datetime NOT NULL,
|
||||
\`date\` datetime NOT NULL,
|
||||
\`memo\` text DEFAULT NULL,
|
||||
\`amount\` bigint(20) NOT NULL,
|
||||
\`moderator\` int UNSIGNED DEFAULT 0,
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE \`login_pending_tasks_admin\`;`)
|
||||
}
|
||||
15
database/ormconfig.js
Normal file
15
database/ormconfig.js
Normal file
@ -0,0 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const CONFIG = require('./src/config')
|
||||
|
||||
module.export = {
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
seeds: ['src/seeds/**/*{.ts,.js}'],
|
||||
factories: ['src/factories/**/*{.ts,.js}'],
|
||||
}
|
||||
@ -16,9 +16,12 @@
|
||||
"dev_up": "nodemon -w ./ --ext ts --exec ts-node src/index.ts up",
|
||||
"dev_down": "nodemon -w ./ --ext ts --exec ts-node src/index.ts down",
|
||||
"dev_reset": "nodemon -w ./ --ext ts --exec ts-node src/index.ts reset",
|
||||
"lint": "eslint . --ext .js,.ts"
|
||||
"lint": "eslint . --ext .js,.ts",
|
||||
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
|
||||
"seed": "nodemon -w ./ --ext ts --exec ts-node src/index.ts seed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/node": "^16.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
@ -35,10 +38,13 @@
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"mysql2": "^2.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-mysql-migrate": "^1.0.2",
|
||||
"typeorm": "^0.2.38"
|
||||
"typeorm": "^0.2.38",
|
||||
"typeorm-seeding": "^1.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
18
database/src/factories/login-user-backup.factory.ts
Normal file
18
database/src/factories/login-user-backup.factory.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { LoginUserBackup } from '../../entity/LoginUserBackup'
|
||||
import { LoginUserBackupContext } from '../interface/UserContext'
|
||||
|
||||
define(LoginUserBackup, (faker: typeof Faker, context?: LoginUserBackupContext) => {
|
||||
if (!context || !context.userId) {
|
||||
throw new Error('LoginUserBackup: No userId present!')
|
||||
}
|
||||
|
||||
const userBackup = new LoginUserBackup()
|
||||
// TODO: Get the real passphrase
|
||||
userBackup.passphrase = context.passphrase ? context.passphrase : faker.random.words(24)
|
||||
userBackup.mnemonicType = context.mnemonicType ? context.mnemonicType : 2
|
||||
userBackup.userId = context.userId
|
||||
|
||||
return userBackup
|
||||
})
|
||||
16
database/src/factories/login-user-roles.factory.ts
Normal file
16
database/src/factories/login-user-roles.factory.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { LoginUserRoles } from '../../entity/LoginUserRoles'
|
||||
import { LoginUserRolesContext } from '../interface/UserContext'
|
||||
|
||||
define(LoginUserRoles, (faker: typeof Faker, context?: LoginUserRolesContext) => {
|
||||
if (!context) context = {}
|
||||
if (!context.userId) throw new Error('LoginUserRoles: No userId present!')
|
||||
if (!context.roleId) throw new Error('LoginUserRoles: No roleId present!')
|
||||
|
||||
const userRoles = new LoginUserRoles()
|
||||
userRoles.userId = context.userId
|
||||
userRoles.roleId = context.roleId
|
||||
|
||||
return userRoles
|
||||
})
|
||||
30
database/src/factories/login-user.factory.ts
Normal file
30
database/src/factories/login-user.factory.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { LoginUser } from '../../entity/LoginUser'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { LoginUserContext } from '../interface/UserContext'
|
||||
|
||||
define(LoginUser, (faker: typeof Faker, context?: LoginUserContext) => {
|
||||
if (!context) context = {}
|
||||
|
||||
const user = new LoginUser()
|
||||
user.email = context.email ? context.email : faker.internet.email()
|
||||
user.firstName = context.firstName ? context.firstName : faker.name.firstName()
|
||||
user.lastName = context.lastName ? context.lastName : faker.name.lastName()
|
||||
user.username = context.username ? context.username : faker.internet.userName()
|
||||
user.description = context.description ? context.description : faker.random.words(4)
|
||||
// TODO Create real password and keys/hash
|
||||
user.password = context.password ? context.password : BigInt(0)
|
||||
user.pubKey = context.pubKey ? context.pubKey : randomBytes(32)
|
||||
user.privKey = context.privKey ? context.privKey : randomBytes(80)
|
||||
user.emailHash = context.emailHash ? context.emailHash : randomBytes(32)
|
||||
user.createdAt = context.createdAt ? context.createdAt : faker.date.recent()
|
||||
user.emailChecked = context.emailChecked ? context.emailChecked : true
|
||||
user.passphraseShown = context.passphraseShown ? context.passphraseShown : false
|
||||
user.language = context.language ? context.language : 'en'
|
||||
user.disabled = context.disabled ? context.disabled : false
|
||||
user.groupId = context.groupId ? context.groupId : 1
|
||||
user.publisherId = context.publisherId ? context.publisherId : 0
|
||||
|
||||
return user
|
||||
})
|
||||
20
database/src/factories/server-user.factory.ts
Normal file
20
database/src/factories/server-user.factory.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { ServerUser } from '../../entity/ServerUser'
|
||||
import { ServerUserContext } from '../interface/UserContext'
|
||||
|
||||
define(ServerUser, (faker: typeof Faker, context?: ServerUserContext) => {
|
||||
if (!context) context = {}
|
||||
|
||||
const user = new ServerUser()
|
||||
user.username = context.username ? context.username : faker.internet.userName()
|
||||
user.password = context.password ? context.password : faker.internet.password()
|
||||
user.email = context.email ? context.email : faker.internet.email()
|
||||
user.role = context.role ? context.role : 'admin'
|
||||
user.activated = context.activated ? context.activated : 0
|
||||
user.lastLogin = context.lastLogin ? context.lastLogin : faker.date.recent()
|
||||
user.created = context.created ? context.created : faker.date.recent()
|
||||
user.modified = context.modified ? context.modified : faker.date.recent()
|
||||
|
||||
return user
|
||||
})
|
||||
21
database/src/factories/user.factory.ts
Normal file
21
database/src/factories/user.factory.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { User } from '../../entity/User'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { UserContext } from '../interface/UserContext'
|
||||
|
||||
define(User, (faker: typeof Faker, context?: UserContext) => {
|
||||
if (!context) context = {}
|
||||
|
||||
const user = new User()
|
||||
user.pubkey = context.pubkey ? context.pubkey : randomBytes(32)
|
||||
user.email = context.email ? context.email : faker.internet.email()
|
||||
user.firstName = context.firstName ? context.firstName : faker.name.firstName()
|
||||
user.lastName = context.lastName ? context.lastName : faker.name.lastName()
|
||||
user.username = context.username ? context.username : faker.internet.userName()
|
||||
user.disabled = context.disabled ? context.disabled : false
|
||||
user.groupId = 0
|
||||
user.indexId = 0
|
||||
|
||||
return user
|
||||
})
|
||||
@ -4,6 +4,11 @@ import { Migration } from 'ts-mysql-migrate'
|
||||
import CONFIG from './config'
|
||||
import prepare from './prepare'
|
||||
import connection from './typeorm/connection'
|
||||
import { useSeeding, runSeeder } from 'typeorm-seeding'
|
||||
import { CreatePeterLustigSeed } from './seeds/users/peter-lustig.admin.seed'
|
||||
import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed'
|
||||
import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed'
|
||||
import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed'
|
||||
|
||||
const run = async (command: string) => {
|
||||
// Database actions not supported by our migration library
|
||||
@ -45,8 +50,20 @@ const run = async (command: string) => {
|
||||
await migration.down() // use for downgrade script
|
||||
break
|
||||
case 'reset':
|
||||
// TODO protect from production
|
||||
await migration.reset() // use for resetting database
|
||||
break
|
||||
case 'seed':
|
||||
// TODO protect from production
|
||||
await useSeeding({
|
||||
root: process.cwd(),
|
||||
configName: 'ormconfig.js',
|
||||
})
|
||||
await runSeeder(CreatePeterLustigSeed)
|
||||
await runSeeder(CreateBibiBloxbergSeed)
|
||||
await runSeeder(CreateRaeuberHotzenplotzSeed)
|
||||
await runSeeder(CreateBobBaumeisterSeed)
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported command ${command}`)
|
||||
}
|
||||
|
||||
49
database/src/interface/UserContext.ts
Normal file
49
database/src/interface/UserContext.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export interface UserContext {
|
||||
pubkey?: Buffer
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
username?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface LoginUserContext {
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
username?: string
|
||||
description?: string
|
||||
password?: BigInt
|
||||
pubKey?: Buffer
|
||||
privKey?: Buffer
|
||||
emailHash?: Buffer
|
||||
createdAt?: Date
|
||||
emailChecked?: boolean
|
||||
passphraseShown?: boolean
|
||||
language?: string
|
||||
disabled?: boolean
|
||||
groupId?: number
|
||||
publisherId?: number | null
|
||||
}
|
||||
|
||||
export interface LoginUserBackupContext {
|
||||
userId?: number
|
||||
passphrase?: string
|
||||
mnemonicType?: number
|
||||
}
|
||||
|
||||
export interface ServerUserContext {
|
||||
username?: string
|
||||
password?: string
|
||||
email?: string
|
||||
role?: string
|
||||
activated?: number
|
||||
lastLogin?: Date
|
||||
created?: Date
|
||||
modified?: Date
|
||||
}
|
||||
|
||||
export interface LoginUserRolesContext {
|
||||
userId?: number
|
||||
roleId?: number
|
||||
}
|
||||
30
database/src/interface/UserInterface.ts
Normal file
30
database/src/interface/UserInterface.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export interface UserInterface {
|
||||
// from login user (contains state user)
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
username?: string
|
||||
description?: string
|
||||
password?: BigInt
|
||||
pubKey?: Buffer
|
||||
privKey?: Buffer
|
||||
emailHash?: Buffer
|
||||
createdAt?: Date
|
||||
emailChecked?: boolean
|
||||
passphraseShown?: boolean
|
||||
language?: string
|
||||
disabled?: boolean
|
||||
groupId?: number
|
||||
publisherId?: number | null
|
||||
// from login user backup
|
||||
passphrase?: string
|
||||
mnemonicType?: number
|
||||
// from server user
|
||||
serverUserPassword?: string
|
||||
role?: string
|
||||
activated?: number
|
||||
lastLogin?: Date
|
||||
modified?: Date
|
||||
// flag for admin
|
||||
isAdmin?: boolean
|
||||
}
|
||||
11
database/src/seeds/create-user.seed.ts
Normal file
11
database/src/seeds/create-user.seed.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Factory, Seeder } from 'typeorm-seeding'
|
||||
import { User } from '../../entity/User'
|
||||
// import { LoginUser } from '../../entity/LoginUser'
|
||||
|
||||
export class CreateUserSeed implements Seeder {
|
||||
public async run(factory: Factory): Promise<void> {
|
||||
// const loginUser = await factory(LoginUser)().make()
|
||||
// console.log(loginUser.email)
|
||||
await factory(User)().create()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user