Merge pull request #1202 from gradido/1197-admin-interface-created-transactions-list

1197 admin interface created transactions list
This commit is contained in:
Alexander Friedland 2021-12-22 12:36:46 +01:00 committed by GitHub
commit 0b9225da09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 804 additions and 177 deletions

View File

@ -0,0 +1,75 @@
import { mount } from '@vue/test-utils'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue()
const toastSuccessMock = jest.fn()
const toastErrorMock = jest.fn()
const mocks = {
$apollo: {
mutate: apolloMutateMock,
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
}
const propsData = {
email: 'bob@baumeister.de',
dateLastSend: '',
}
describe('ConfirmRegisterMailFormular', () => {
let wrapper
const Wrapper = () => {
return mount(ConfirmRegisterMailFormular, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class.component-confirm-register-mail', () => {
expect(wrapper.find('.component-confirm-register-mail').exists()).toBeTruthy()
})
describe('send register mail with success', () => {
beforeEach(() => {
wrapper.find('button.test-button').trigger('click')
})
it('calls the API with email', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: { email: 'bob@baumeister.de' },
}),
)
})
it('toasts a success message', () => {
expect(toastSuccessMock).toBeCalledWith(
'Erfolgreich senden der Confirmation Link an die E-Mail des Users! bob@baumeister.de',
)
})
})
describe('send register mail with error', () => {
beforeEach(() => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
wrapper.find('button.test-button').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith(
'Fehler beim senden des confirmation link an den Benutzer: OUCH!',
)
})
})
})
})

View File

@ -0,0 +1,62 @@
<template>
<div class="component-confirm-register-mail">
<div class="shadow p-3 mb-5 bg-white rounded">
<div class="h5">
Die letzte Email wurde am
<b>{{ dateLastSend }} Uhr</b>
an das Mitglied ({{ email }}) gesendet.
</div>
<!-- Using components -->
<b-input-group prepend="Email bestätigen, wiederholt senden an:" class="mt-3">
<b-form-input readonly :value="email"></b-form-input>
<b-input-group-append>
<b-button variant="outline-success" class="test-button" @click="sendRegisterMail">
Registrierungs-Email bestätigen, jetzt senden
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
</template>
<script>
import { sendActivationEmail } from '../graphql/sendActivationEmail'
export default {
name: 'ConfirmRegisterMail',
props: {
email: {
type: String,
},
dateLastSend: {
type: String,
},
},
methods: {
sendRegisterMail() {
this.$apollo
.mutate({
mutation: sendActivationEmail,
variables: {
email: this.email,
},
})
.then(() => {
this.$toasted.success(
'Erfolgreich senden der Confirmation Link an die E-Mail des Users! ' + this.email,
)
})
.catch((error) => {
this.$toasted.error(
'Fehler beim senden des confirmation link an den Benutzer: ' + error.message,
)
})
},
},
}
</script>
<style>
.input-group-text {
background-color: rgb(255, 252, 205);
}
</style>

View File

@ -176,8 +176,8 @@ describe('CreationFormular', () => {
await wrapper.find('.test-submit').trigger('click')
})
it('sends ... to apollo', () => {
expect(toastedErrorMock).toBeCalled()
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})

View File

@ -1,5 +1,6 @@
<template>
<div class="component-creation-formular">
CREATION FORMULAR
<div class="shadow p-3 mb-5 bg-white rounded">
<b-form ref="creationForm">
<b-row class="m-4">
@ -204,8 +205,6 @@ export default {
// 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)
// eslint-disable-next-line no-console
console.log('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
this.submitObj = [
{
item: this.itemsMassCreation,
@ -216,8 +215,6 @@ export default {
moderator: this.$store.state.moderator.id,
},
]
// eslint-disable-next-line no-console
console.log('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
// $store - offene Schöpfungen hochzählen
this.$store.commit('openCreationsPlus', i)

View File

@ -0,0 +1,115 @@
import { mount } from '@vue/test-utils'
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
transactionList: {
transactions: [
{
type: 'created',
balance: 100,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
memo: 'Testing',
transactionId: 1,
name: 'Bibi',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
balance: 0.01,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
decayStartBlock: 0,
},
},
{
type: 'created',
balance: 200,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
memo: 'Testing 2',
transactionId: 2,
name: 'Bibi',
email: 'bibi@bloxberg.de',
date: new Date(),
decay: {
balance: 0.01,
decayStart: 0,
decayEnd: 0,
decayDuration: 0,
decayStartBlock: 0,
},
},
],
},
},
})
const toastedErrorMock = jest.fn()
const mocks = {
$apollo: {
query: apolloQueryMock,
},
$toasted: {
global: {
error: toastedErrorMock,
},
},
}
const propsData = {
userId: 1,
}
describe('CreationTransactionListFormular', () => {
let wrapper
const Wrapper = () => {
return mount(CreationTransactionListFormular, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: 1,
},
}),
)
})
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
it('toast error', () => {
expect(toastedErrorMock).toBeCalledWith('OUCH!')
})
})
})
})

View File

@ -0,0 +1,44 @@
<template>
<div class="component-creation-transaction-list">
Alle Geschöpften Transaktionen für den User
<b-table striped hover :items="items"></b-table>
</div>
</template>
<script>
import { transactionList } from '../graphql/transactionList'
export default {
name: 'CreationTransactionList',
props: {
userId: { type: Number, required: true },
},
data() {
return {
items: [],
}
},
methods: {
getTransactions() {
this.$apollo
.query({
query: transactionList,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
onlyCreations: true,
userId: parseInt(this.userId),
},
})
.then((result) => {
this.items = result.data.transactionList.transactions
})
.catch((error) => {
this.$toasted.global.error(error.message)
})
},
},
created() {
this.getTransactions()
},
}
</script>

View File

@ -46,9 +46,12 @@ const mocks = {
}
const propsData = {
type: '',
creation: [],
itemsMassCreation: {},
creation: [200, 400, 600],
creationUserData: {
memo: 'Test schöpfung 1',
amount: 100,
date: '2021-12-01',
},
}
describe('EditCreationFormular', () => {
@ -67,7 +70,7 @@ describe('EditCreationFormular', () => {
expect(wrapper.find('.component-edit-creation-formular').exists()).toBeTruthy()
})
describe('radio buttons to selcet month', () => {
describe('radio buttons to select month', () => {
it('has three radio buttons', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
})
@ -75,7 +78,7 @@ describe('EditCreationFormular', () => {
describe('with single creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
@ -113,6 +116,26 @@ describe('EditCreationFormular', () => {
}),
)
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
await wrapper.setData({ rangeMin: 100 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
})
})
@ -148,23 +171,44 @@ describe('EditCreationFormular', () => {
}),
)
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 100 })
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.setData({ rangeMin: 180 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.setData({ rangeMin: 180 })
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
it('sets rangeMin to 180', () => {
expect(wrapper.vm.rangeMin).toBe(180)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(600)
it('sets rangeMax to 700', () => {
expect(wrapper.vm.rangeMax).toBe(700)
})
describe('sendForm', () => {
describe('sendForm with success', () => {
beforeEach(async () => {
await wrapper.find('.test-submit').trigger('click')
})
@ -184,6 +228,26 @@ describe('EditCreationFormular', () => {
)
})
})
describe('sendForm with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Ouch!',
})
wrapper = Wrapper()
await wrapper.setProps({ creation: [200, 400, 600] })
await wrapper.setData({ text: 'Test create coins' })
await wrapper.setData({ value: 90 })
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
await wrapper.setData({ rangeMin: 180 })
await wrapper.find('.test-submit').trigger('click')
})
it('toast error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})
})
})
})

View File

@ -113,7 +113,7 @@
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
>
Update Schöpfung ({{ type }},{{ pagetype }})
Update Schöpfung
</b-button>
</div>
</b-col>
@ -127,15 +127,6 @@ import { updatePendingCreation } from '../graphql/updatePendingCreation'
export default {
name: 'EditCreationFormular',
props: {
type: {
type: String,
required: false,
},
pagetype: {
type: String,
required: false,
default: '',
},
item: {
type: Object,
required: false,
@ -143,13 +134,6 @@ export default {
return {}
},
},
items: {
type: Array,
required: false,
default() {
return []
},
},
row: {
type: Object,
required: false,
@ -247,7 +231,7 @@ export default {
},
},
created() {
if (this.pagetype === 'PageCreationConfirm' && this.creationUserData.date) {
if (this.creationUserData.date) {
switch (this.$moment(this.creationUserData.date).format('MMMM')) {
case this.currentMonth.short:
this.createdIndex = 2

View File

@ -1,6 +1,6 @@
<template>
<div class="component-nabvar">
<b-navbar toggleable="sm" type="dark" variant="success">
<b-navbar toggleable="md" type="dark" variant="success" class="p-3">
<b-navbar-brand to="/">
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
</b-navbar-brand>
@ -9,19 +9,18 @@
<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="/">Ü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"
class="bg-color-creation p-1"
to="/creation-confirm"
>
| {{ $store.state.openCreations }} offene Schöpfungen
{{ $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>
@ -49,4 +48,7 @@ export default {
height: 2rem;
padding-left: 10px;
}
.bg-color-creation {
background-color: #cf1010dc;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<b-card class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
<b-row class="mb-2">
<b-col></b-col>
</b-row>
<slot :name="slotName" />
<b-button size="sm" @click="$emit('row-toogle-details', row, index)">
<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>
<script>
export default {
name: 'RowDetails',
props: {
row: { required: true, type: Object },
slotName: { requried: true, type: String },
type: { requried: true, type: String },
index: { requried: true, type: Number },
},
}
</script>

View File

@ -37,62 +37,81 @@
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 variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'x' : '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 variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'eye-slash-fill' : 'eye-fill'"></b-icon>
</b-button>
</template>
<template #cell(confirm_mail)="row">
<b-button
:variant="row.item.emailChecked ? 'success' : 'danger'"
size="md"
@click="rowToogleDetails(row, 1)"
class="mr-2"
>
<b-icon
:icon="row.item.emailChecked ? 'envelope-open' : 'envelope'"
aria-label="Help"
></b-icon>
</b-button>
</template>
<template #cell(transactions_list)="row">
<b-button variant="warning" size="md" @click="rowToogleDetails(row, 2)" class="mr-2">
<b-icon icon="list"></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>
{{ type }}
<creation-formular
v-if="type === 'PageUserSearch'"
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<edit-creation-formular
v-else
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@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>
<row-details
:row="row"
:type="type"
:slotName="slotName"
:index="slotIndex"
@row-toogle-details="rowToogleDetails"
>
<template #show-creation>
<div>
<creation-formular
v-if="type === 'PageUserSearch'"
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<edit-creation-formular
v-else
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
</div>
</template>
<template #show-register-mail>
<confirm-register-mail-formular
:email="row.item.email"
:dateLastSend="$moment().subtract(1, 'month').format('dddd, DD.MMMM.YYYY HH:mm'),"
/>
</template>
<template #show-transaction-list>
<creation-transaction-list-formular :userId="row.item.userId" />
</template>
</row-details>
</template>
<template #cell(bookmark)="row">
<b-button
variant="warning"
@ -132,8 +151,14 @@
<script>
import CreationFormular from '../components/CreationFormular.vue'
import EditCreationFormular from '../components/EditCreationFormular.vue'
import ConfirmRegisterMailFormular from '../components/ConfirmRegisterMailFormular.vue'
import CreationTransactionListFormular from '../components/CreationTransactionListFormular.vue'
import RowDetails from '../components/RowDetails.vue'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
const slotNames = ['show-creation', 'show-register-mail', 'show-transaction-list']
export default {
name: 'UserTable',
props: {
@ -162,9 +187,15 @@ export default {
components: {
CreationFormular,
EditCreationFormular,
ConfirmRegisterMailFormular,
CreationTransactionListFormular,
RowDetails,
},
data() {
return {
showCreationFormular: null,
showConfirmRegisterMailFormular: null,
showCreationTransactionListFormular: null,
creationUserData: {},
overlay: false,
overlayBookmarkType: '',
@ -178,9 +209,35 @@ export default {
button_cancel: 'Cancel',
},
],
slotIndex: 0,
openRow: null,
}
},
methods: {
rowToogleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
}
} else {
row.toggleDetails()
this.slotIndex = index
this.openRow = row
if (this.type === 'PageCreationConfirm') {
this.creationUserData = row.item
}
}
},
overlayShow(bookmarkType, item) {
this.overlay = true
this.overlayBookmarkType = bookmarkType
@ -243,14 +300,6 @@ export default {
this.$toasted.error(error.message)
})
},
editCreationUserTable(row, rowItem) {
if (!row.detailsShowing) {
this.creationUserData = rowItem
} else {
this.creationUserData = {}
}
row.toggleDetails()
},
updateCreationData(data) {
this.creationUserData.amount = data.amount
this.creationUserData.date = data.date
@ -263,6 +312,11 @@ export default {
rowItem.creation = newCreation
},
},
computed: {
slotName() {
return slotNames[this.slotIndex]
},
},
}
</script>
<style>

View File

@ -3,10 +3,12 @@ import gql from 'graphql-tag'
export const searchUsers = gql`
query ($searchText: String!) {
searchUsers(searchText: $searchText) {
userId
firstName
lastName
email
creation
emailChecked
}
}
`

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const sendActivationEmail = gql`
mutation ($email: String!) {
sendActivationEmail(email: $email)
}
`

View File

@ -0,0 +1,44 @@
import gql from 'graphql-tag'
export const transactionList = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$onlyCreations: Boolean = false
$userId: Int = null
) {
transactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
userId: $userId
) {
gdtSum
count
balance
decay
decayDate
transactions {
type
balance
decayStart
decayEnd
decayDuration
memo
transactionId
name
email
date
decay {
balance
decayStart
decayEnd
decayDuration
decayStartBlock
}
}
}
}
`

View File

@ -40,7 +40,6 @@
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmark"
/>
{{ itemsMassCreation }}
</b-col>
</b-row>
</div>

View File

@ -28,28 +28,6 @@
</b-link>
</b-card-text>
</b-card>
<br />
<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>2400</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>2201</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>120</b-badge>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>

View File

@ -11,6 +11,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: false,
},
],
},
@ -43,6 +44,16 @@ describe('UserSearch', () => {
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
})
describe('unconfirmed emails', () => {
beforeEach(async () => {
await wrapper.find('button.btn-block').trigger('click')
})
it('filters the users by unconfirmed emails', () => {
expect(wrapper.vm.searchResult).toHaveLength(0)
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({

View File

@ -4,16 +4,23 @@
<b-input
type="text"
v-model="criteria"
class="shadow p-3 mb-5 bg-white rounded"
class="shadow p-3 mb-3 bg-white rounded"
placeholder="User suche"
@input="getUsers"
></b-input>
<user-table
type="PageUserSearch"
:itemsUser="searchResult"
:fieldsTable="fields"
:criteria="criteria"
/>
<div>
<b-button block variant="danger" @click="unconfirmedRegisterMails">
<b-icon icon="envelope" variant="light"></b-icon>
Anzeigen aller nicht registrierten E-Mails.
</b-button>
</div>
</div>
</template>
<script>
@ -40,6 +47,8 @@ export default {
},
},
{ key: 'show_details', label: 'Details' },
{ key: 'confirm_mail', label: 'Mail' },
{ key: 'transactions_list', label: 'Transaction' },
],
searchResult: [],
massCreation: [],
@ -48,6 +57,11 @@ export default {
},
methods: {
unconfirmedRegisterMails() {
this.searchResult = this.searchResult.filter((user) => {
return user.emailChecked
})
},
getUsers() {
this.$apollo
.query({
@ -57,12 +71,7 @@ export default {
},
})
.then((result) => {
this.searchResult = result.data.searchUsers.map((user) => {
return {
...user,
// showDetails: true,
}
})
this.searchResult = result.data.searchUsers
})
.catch((error) => {
this.$toasted.error(error.message)

View File

@ -15,6 +15,9 @@ DB_DATABASE=gradido_community
#EMAIL_PASSWORD=
#EMAIL_SMTP_URL=
#EMAIL_SMTP_PORT=587
#RESEND_TIME=1 minute, 60 => 1hour, 1440 (60 minutes * 24 hours) => 24 hours
#RESEND_TIME=
RESEND_TIME=10
#EMAIL_LINK_VERIFICATION=http://localhost/vue/checkEmail/$1

View File

@ -42,6 +42,7 @@ const loginServer = {
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const resendTime = parseInt(process.env.RESEND_TIME ? process.env.RESEND_TIME : 'null')
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
@ -52,6 +53,7 @@ const email = {
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
RESEND_TIME: isNaN(resendTime) ? 10 : resendTime,
}
const webhook = {

View File

@ -11,4 +11,10 @@ export default class Paginated {
@Field(() => Order, { nullable: true })
order?: Order
@Field(() => Boolean, { nullable: true })
onlyCreations?: boolean
@Field(() => Int, { nullable: true })
userId?: number
}

View File

@ -2,6 +2,9 @@ import { ObjectType, Field } from 'type-graphql'
@ObjectType()
export class UserAdmin {
@Field(() => Number)
userId: number
@Field(() => String)
email: string
@ -13,4 +16,7 @@ export class UserAdmin {
@Field(() => [Number])
creation: number[]
@Field(() => Boolean)
emailChecked: boolean
}

View File

@ -17,6 +17,7 @@ import { UserTransaction } from '@entity/UserTransaction'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { calculateDecay } from '../../util/decay'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
@Resolver()
export class AdminResolver {
@ -28,10 +29,12 @@ export class AdminResolver {
const adminUsers = await Promise.all(
users.map(async (user) => {
const adminUser = new UserAdmin()
adminUser.userId = user.id
adminUser.firstName = user.firstName
adminUser.lastName = user.lastName
adminUser.email = user.email
adminUser.creation = await getUserCreations(user.id)
adminUser.emailChecked = await hasActivatedEmail(user.email)
return adminUser
}),
)
@ -315,3 +318,10 @@ function isCreationValid(creations: number[], amount: number, creationDate: Date
}
return true
}
async function hasActivatedEmail(email: string): Promise<boolean> {
const repository = getCustomRepository(LoginUserRepository)
const user = await repository.findByEmail(email)
let emailActivate = false
if (user) emailActivate = user.emailChecked
return user ? user.emailChecked : false
}

View File

@ -340,6 +340,7 @@ async function listTransactions(
pageSize: number,
order: Order,
user: dbUser,
onlyCreations: boolean,
): Promise<TransactionList> {
let limit = pageSize
let offset = 0
@ -358,6 +359,7 @@ async function listTransactions(
limit,
offset,
order,
onlyCreations,
)
skipFirstTransaction = userTransactionsCount > offset + limit
const decay = !(currentPage > 1)
@ -469,14 +471,32 @@ export class TransactionResolver {
@Authorized([RIGHTS.TRANSACTION_LIST])
@Query(() => TransactionList)
async transactionList(
@Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Args()
{
currentPage = 1,
pageSize = 25,
order = Order.DESC,
onlyCreations = false,
userId,
}: Paginated,
@Ctx() context: any,
): Promise<TransactionList> {
// load user
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
let userEntity: dbUser | undefined
if (userId) {
userEntity = await userRepository.findOneOrFail({ id: userId })
} else {
userEntity = await userRepository.findByPubkeyHex(context.pubKey)
}
const transactions = await listTransactions(currentPage, pageSize, order, userEntity)
const transactions = await listTransactions(
currentPage,
pageSize,
order,
userEntity,
onlyCreations,
)
// get gdt sum
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {

View File

@ -3,7 +3,7 @@
import fs from 'fs'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository, getRepository } from 'typeorm'
import { getConnection, getCustomRepository, getRepository, QueryRunner } from 'typeorm'
import CONFIG from '../../config'
import { User } from '../model/User'
import { User as DbUser } from '@entity/User'
@ -148,6 +148,66 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
return message
}
const createEmailOptIn = async (
loginUserId: number,
queryRunner: QueryRunner,
): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let emailOptIn = await loginEmailOptInRepository.findOne({
userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
})
if (emailOptIn) {
const timeElapsed = Date.now() - new Date(emailOptIn.updatedAt).getTime()
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
throw new Error(
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
)
} else {
emailOptIn.updatedAt = new Date()
emailOptIn.resendCount++
}
} else {
emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64)
emailOptIn.userId = loginUserId
emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
}
await queryRunner.manager.save(emailOptIn).catch((error) => {
// eslint-disable-next-line no-console
console.log('Error while saving emailOptIn', error)
throw new Error('error saving email opt in')
})
return emailOptIn
}
const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({
userId: loginUser.id,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
// Check for 10 minute delay
if (optInCode) {
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
throw new Error(
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
)
} else {
optInCode.updatedAt = new Date()
optInCode.resendCount++
}
} else {
optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64)
optInCode.userId = loginUser.id
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
}
await loginEmailOptInRepository.save(optInCode)
return optInCode
}
@Resolver()
export class UserResolver {
@ -383,37 +443,18 @@ export class UserResolver {
// Store EmailOptIn in DB
// TODO: this has duplicate code with sendResetPasswordEmail
const emailOptIn = new LoginEmailOptIn()
emailOptIn.userId = loginUserId
emailOptIn.verificationCode = random(64)
emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
const emailOptIn = await createEmailOptIn(loginUserId, queryRunner)
await queryRunner.manager.save(emailOptIn).catch((error) => {
// eslint-disable-next-line no-console
console.log('Error while saving emailOptIn', error)
throw new Error('error saving email opt in')
})
// Send EMail to user
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/\$1/g,
emailOptIn.verificationCode.toString(),
)
const emailSent = await sendEMail({
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${firstName} ${lastName} <${email}>`,
subject: 'Gradido: E-Mail Überprüfung',
text: `Hallo ${firstName} ${lastName},
Deine EMail wurde soeben bei Gradido registriert.
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
${activationLink}
oder kopiere den obigen Link in dein Browserfenster.
Mit freundlichen Grüßen,
dein Gradido-Team`,
})
const emailSent = await this.sendAccountActivationEmail(
activationLink,
firstName,
lastName,
email,
)
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
@ -430,33 +471,78 @@ export class UserResolver {
return 'success'
}
private async sendAccountActivationEmail(
activationLink: string,
firstName: string,
lastName: string,
email: string,
) {
const emailSent = await sendEMail({
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${firstName} ${lastName} <${email}>`,
subject: 'Gradido: E-Mail Überprüfung',
text: `Hallo ${firstName} ${lastName},
Deine EMail wurde soeben bei Gradido registriert.
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
${activationLink}
oder kopiere den obigen Link in dein Browserfenster.
Mit freundlichen Grüßen,
dein Gradido-Team`,
})
return emailSent
}
@Mutation(() => Boolean)
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findOneOrFail({ email: email })
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
const emailOptIn = await createEmailOptIn(loginUser.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/\$1/g,
emailOptIn.verificationCode.toString(),
)
const emailSent = await this.sendAccountActivationEmail(
activationLink,
loginUser.firstName,
loginUser.lastName,
email,
)
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
// eslint-disable-next-line no-console
console.log(`Account confirmation link: ${activationLink}`)
}
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
}
return true
}
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Query(() => Boolean)
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
// TODO: this has duplicate code with createUser
const loginUserRepository = await getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findOneOrFail({ email })
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({
userId: loginUser.id,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
// Check for 10 minute delay
if (optInCode) {
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
if (timeElapsed <= 10 * 60 * 1000) {
throw new Error('email already sent less than 10 minutes before')
}
}
// Generate new OptIn Code
optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64)
optInCode.userId = loginUser.id
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
await loginEmailOptInRepository.save(optInCode)
const optInCode = await getOptInCode(loginUser)
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
/\$1/g,

View File

@ -9,7 +9,17 @@ export class UserTransactionRepository extends Repository<UserTransaction> {
limit: number,
offset: number,
order: Order,
onlyCreation?: boolean,
): Promise<[UserTransaction[], number]> {
if (onlyCreation) {
return this.createQueryBuilder('userTransaction')
.where('userTransaction.userId = :userId', { userId })
.andWhere('userTransaction.type = "creation"')
.orderBy('userTransaction.balanceDate', order)
.limit(limit)
.offset(offset)
.getManyAndCount()
}
return this.createQueryBuilder('userTransaction')
.where('userTransaction.userId = :userId', { userId })
.orderBy('userTransaction.balanceDate', order)

View File

@ -47,8 +47,18 @@ export const logout = gql`
`
export const transactionsQuery = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
query(
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$onlyCreations: Boolean = false
) {
transactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
) {
gdtSum
count
balance