Merge remote-tracking branch 'origin/master' into 1794-feature-event-protocol-1-implement-the-basics-of-the-business-event-protocol
@ -1,3 +0,0 @@
|
||||
4ƒk׀ךֻ1°,•<EFBFBD>fעbלAלqִ¬cי<EFBFBD>#¾›צ¾ר#s’8-ְ1‰&»;נצד"¢פשל7€d¥jM?‘bljfB¼ƒqֱ=
|
||||
<EFBFBD>ײmyפ¿
|
||||
vת·´V
|
||||
@ -1,32 +0,0 @@
|
||||
{
|
||||
"folders": {},
|
||||
"connections": {
|
||||
"mariaDB-1813fbbc7bc-107c0b3aeaeb91ab": {
|
||||
"provider": "mysql",
|
||||
"driver": "mariaDB",
|
||||
"name": "gradido",
|
||||
"save-password": true,
|
||||
"read-only": false,
|
||||
"configuration": {
|
||||
"host": "localhost",
|
||||
"port": "3306",
|
||||
"url": "jdbc:mariadb://localhost:3306/",
|
||||
"home": "mysql_client",
|
||||
"type": "dev",
|
||||
"auth-model": "native",
|
||||
"handlers": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connection-types": {
|
||||
"dev": {
|
||||
"name": "Development",
|
||||
"color": "255,255,255",
|
||||
"description": "Regular development database",
|
||||
"auto-commit": true,
|
||||
"confirm-execute": false,
|
||||
"confirm-data-change": false,
|
||||
"auto-close-transactions": false
|
||||
}
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.dbeaver
|
||||
.project
|
||||
*.log
|
||||
*.bak
|
||||
/node_modules/*
|
||||
|
||||
18
.project
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>Gradido</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.wst.validation.validationbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.jkiss.dbeaver.DBeaverNature</nature>
|
||||
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
254
admin/src/components/ChangeUserRoleFormular.spec.js
Normal file
@ -0,0 +1,254 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let propsData
|
||||
let wrapper
|
||||
|
||||
describe('ChangeUserRoleFormular', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(ChangeUserRoleFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('DOM has', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.change-user-role-formular').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change own role', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 0,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has the text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has role select disabled', () => {
|
||||
expect(wrapper.find('select[disabled="disabled"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change others role', () => {
|
||||
let rolesToSelect
|
||||
|
||||
describe('general', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has no text that you cannot change own role', () => {
|
||||
expect(wrapper.text()).not.toContain('userRole.notChangeYourSelf')
|
||||
})
|
||||
|
||||
it('has the select label', () => {
|
||||
expect(wrapper.text()).toContain('userRole.selectLabel')
|
||||
})
|
||||
|
||||
it('has a select', () => {
|
||||
expect(wrapper.find('select.role-select').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has role select enabled', () => {
|
||||
expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('on API error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is usual user', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: new Date(),
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "usual user"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('user')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: expect.any(Date),
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is admin', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
})
|
||||
propsData = {
|
||||
item: {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
},
|
||||
}
|
||||
wrapper = Wrapper()
|
||||
rolesToSelect = wrapper.find('select.role-select').findAll('option')
|
||||
})
|
||||
|
||||
it('has selected option set to "admin"', () => {
|
||||
expect(wrapper.find('select.role-select').element.value).toBe('admin')
|
||||
})
|
||||
|
||||
describe('change select to', () => {
|
||||
describe('same role', () => {
|
||||
it('does not call the API', () => {
|
||||
rolesToSelect.at(1).setSelected()
|
||||
expect(apolloMutateMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('new role', () => {
|
||||
beforeEach(() => {
|
||||
rolesToSelect.at(0).setSelected()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: 1,
|
||||
isAdmin: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits "updateIsAdmin"', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
isAdmin: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
89
admin/src/components/ChangeUserRoleFormular.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="change-user-role-formular">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
|
||||
{{ $t('userRole.notChangeYourSelf') }}
|
||||
</div>
|
||||
<div class="m-3">
|
||||
<label for="role" class="mr-3">{{ $t('userRole.selectLabel') }}</label>
|
||||
<b-form-select
|
||||
class="role-select"
|
||||
v-model="roleSelected"
|
||||
:options="roles"
|
||||
:disabled="item.userId === $store.state.moderator.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setUserRole } from '../graphql/setUserRole'
|
||||
|
||||
const rolesValues = {
|
||||
admin: 'admin',
|
||||
user: 'user',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ChangeUserRoleFormular',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
|
||||
roles: [
|
||||
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
|
||||
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
|
||||
],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roleSelected(newRole, oldRole) {
|
||||
if (newRole !== oldRole) {
|
||||
this.setUserRole(newRole, oldRole)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setUserRole(newRole, oldRole) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: setUserRole,
|
||||
variables: {
|
||||
userId: this.item.userId,
|
||||
isAdmin: newRole === rolesValues.admin,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('updateIsAdmin', {
|
||||
userId: this.item.userId,
|
||||
isAdmin: result.data.setUserRole,
|
||||
})
|
||||
this.toastSuccess(
|
||||
this.$t('userRole.successfullyChangedTo', {
|
||||
role:
|
||||
result.data.setUserRole !== null
|
||||
? this.$t('userRole.selectRoles.admin')
|
||||
: this.$t('userRole.selectRoles.user'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.roleSelected = oldRole
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.role-select {
|
||||
width: 300pt;
|
||||
}
|
||||
</style>
|
||||
@ -47,200 +47,200 @@ describe('DeletedUserFormular', () => {
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,6 +28,7 @@ export default {
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SearchUserTable from './SearchUserTable.vue'
|
||||
|
||||
const date = new Date()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
@ -96,16 +94,29 @@ describe('SearchUserTable', () => {
|
||||
await wrapper.findAll('tbody > tr').at(1).trigger('click')
|
||||
})
|
||||
|
||||
describe('isAdmin', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
|
||||
userId: 1,
|
||||
isAdmin: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateIsAdmin', () => {
|
||||
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleted at', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
deletedAt: new Date(),
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateDeletedAt', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]])
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, expect.any(Date)]])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="text-right">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3 test-deleted-icon" variant="light">
|
||||
<b-iconstack font-scale="2">
|
||||
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
|
||||
<b-icon stacked icon="slash-circle" variant="danger"></b-icon>
|
||||
@ -79,6 +79,9 @@
|
||||
<b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null">
|
||||
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('userRole.tabTitle')">
|
||||
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
|
||||
</b-tab>
|
||||
<b-tab :title="$t('delete_user')">
|
||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||
</b-tab>
|
||||
@ -93,6 +96,7 @@ import CreationFormular from '../CreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionList from '../CreationTransactionList.vue'
|
||||
import TransactionLinkList from '../TransactionLinkList.vue'
|
||||
import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
|
||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||
|
||||
export default {
|
||||
@ -102,6 +106,7 @@ export default {
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionList,
|
||||
TransactionLinkList,
|
||||
ChangeUserRoleFormular,
|
||||
DeletedUserFormular,
|
||||
},
|
||||
props: {
|
||||
@ -123,6 +128,9 @@ export default {
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
updateIsAdmin({ userId, isAdmin }) {
|
||||
this.$emit('updateIsAdmin', userId, isAdmin)
|
||||
},
|
||||
updateDeletedAt({ userId, deletedAt }) {
|
||||
this.$emit('updateDeletedAt', userId, deletedAt)
|
||||
},
|
||||
|
||||
@ -19,6 +19,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
admin/src/graphql/setUserRole.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const setUserRole = gql`
|
||||
mutation ($userId: Int!, $isAdmin: Boolean!) {
|
||||
setUserRole(userId: $userId, isAdmin: $isAdmin)
|
||||
}
|
||||
`
|
||||
@ -101,7 +101,7 @@
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"remove": "Entfernen",
|
||||
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
@ -131,6 +131,16 @@
|
||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
||||
"text_true": " Die Email wurde bestätigt."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "Als Admin/Moderator kannst du nicht selber deine Rolle ändern.",
|
||||
"selectLabel": "Rolle:",
|
||||
"selectRoles": {
|
||||
"admin": "Administrator",
|
||||
"user": "einfacher Nutzer"
|
||||
},
|
||||
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
|
||||
"tabTitle": "Nutzer-Rolle"
|
||||
},
|
||||
"user_deleted": "Nutzer ist gelöscht.",
|
||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||
"user_search": "Nutzer-Suche"
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
@ -131,6 +131,16 @@
|
||||
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
||||
"text_true": "The email was confirmed."
|
||||
},
|
||||
"userRole": {
|
||||
"notChangeYourSelf": "As an admin/moderator, you cannot change your own role.",
|
||||
"selectLabel": "Role:",
|
||||
"selectRoles": {
|
||||
"admin": "administrator",
|
||||
"user": "usual user"
|
||||
},
|
||||
"successfullyChangedTo": "User is now \"{role}\".",
|
||||
"tabTitle": "User Role"
|
||||
},
|
||||
"user_deleted": "User is deleted.",
|
||||
"user_recovered": "User is recovered.",
|
||||
"user_search": "User search"
|
||||
|
||||
@ -199,14 +199,43 @@ describe('UserSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('change user role', () => {
|
||||
const userId = 4
|
||||
|
||||
describe('to admin', () => {
|
||||
it('updates user role to admin', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, new Date())
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
it('updates user role to usual user', async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateIsAdmin', userId, null)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
const now = new Date()
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
|
||||
const userId = 4
|
||||
beforeEach(() => {
|
||||
wrapper
|
||||
.findComponent({ name: 'SearchUserTable' })
|
||||
.vm.$emit('updateDeletedAt', userId, new Date())
|
||||
})
|
||||
|
||||
it('marks the user as deleted', () => {
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).deletedAt).toEqual(
|
||||
expect.any(Date),
|
||||
)
|
||||
expect(wrapper.find('.test-deleted-icon').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
type="PageUserSearch"
|
||||
:items="searchResult"
|
||||
:fields="fields"
|
||||
@updateIsAdmin="updateIsAdmin"
|
||||
@updateDeletedAt="updateDeletedAt"
|
||||
/>
|
||||
<b-pagination
|
||||
@ -111,6 +112,9 @@ export default {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateIsAdmin(userId, isAdmin) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
|
||||
},
|
||||
updateDeletedAt(userId, deletedAt) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
|
||||
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v7.2022-06-15
|
||||
CONFIG_VERSION=v8.2022-06-20
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
|
||||
@ -43,6 +43,7 @@ EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
||||
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
||||
EMAIL_LINK_FORGOTPASSWORD=$EMAIL_LINK_FORGOTPASSWORD
|
||||
EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW
|
||||
EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
|
||||
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
||||
|
||||
@ -27,6 +27,9 @@ export enum RIGHTS {
|
||||
GDT_BALANCE = 'GDT_BALANCE',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
|
||||
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
@ -34,8 +37,6 @@ export enum RIGHTS {
|
||||
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
|
||||
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
|
||||
@ -17,7 +17,7 @@ const constants = {
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v7.2022-06-15',
|
||||
EXPECTED: 'v8.2022-06-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export class UserAdmin {
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
this.isAdmin = user.isAdmin
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -42,6 +43,9 @@ export class UserAdmin {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend?: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
isAdmin: Date | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@ -13,6 +13,7 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import {
|
||||
setUserRole,
|
||||
deleteUser,
|
||||
unDeleteUser,
|
||||
adminCreateContribution,
|
||||
@ -69,6 +70,161 @@ let user: User
|
||||
let creation: Contribution | void
|
||||
|
||||
describe('AdminResolver', () => {
|
||||
describe('set user role', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: 1, isAdmin: true } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: user.id + 1, isAdmin: true } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('user to get a new role does not exist', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('change role with success', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
})
|
||||
|
||||
describe('user gets new role', () => {
|
||||
describe('to admin', () => {
|
||||
it('returns date string', async () => {
|
||||
const result = await mutate({
|
||||
mutation: setUserRole,
|
||||
variables: { userId: user.id, isAdmin: true },
|
||||
})
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
setUserRole: expect.any(String),
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(new Date(result.data.setUserRole)).toEqual(expect.any(Date))
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
setUserRole: null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('change role with error', () => {
|
||||
describe('is own role', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Administrator can not change his own role!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has already role to be set', () => {
|
||||
describe('to admin', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: setUserRole,
|
||||
variables: { userId: user.id, isAdmin: true },
|
||||
})
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User is already admin!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('to usual user', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: setUserRole,
|
||||
variables: { userId: user.id, isAdmin: false },
|
||||
})
|
||||
await expect(
|
||||
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User is already a usual user!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
@ -1807,6 +1963,189 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if missing startDate', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
validFrom: null,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('Start-Date is not initialized. A Start-Date must be set!'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if missing endDate', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
validTo: null,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('End-Date is not initialized. An End-Date must be set!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if endDate is before startDate', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
validFrom: new Date('2022-06-18T00:00:00.001Z').toISOString(),
|
||||
validTo: new Date('2022-06-18T00:00:00.000Z').toISOString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(`The value of validFrom must before or equals the validTo!`),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The name must be initialized!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is shorter than 5 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '123',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is longer than 100 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
name: '12345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is an empty string', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The memo must be initialized!')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '123',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is longer than 255 characters', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if amount is not positive', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
...variables,
|
||||
amount: new Decimal(0),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('The amount=0 must be initialized with a positiv value!'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('listContributionLinks', () => {
|
||||
|
||||
@ -51,6 +51,10 @@ import CONFIG from '@/config'
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
||||
const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||
const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@ -73,7 +77,15 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'email',
|
||||
'emailChecked',
|
||||
'deletedAt',
|
||||
'isAdmin',
|
||||
]
|
||||
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
|
||||
userFields.map((fieldName) => {
|
||||
return 'user.' + fieldName
|
||||
@ -133,6 +145,48 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SET_USER_ROLE])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async setUserRole(
|
||||
@Arg('userId', () => Int)
|
||||
userId: number,
|
||||
@Arg('isAdmin', () => Boolean)
|
||||
isAdmin: boolean,
|
||||
@Ctx()
|
||||
context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await dbUser.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// administrator user changes own role?
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === userId) {
|
||||
throw new Error('Administrator can not change his own role!')
|
||||
}
|
||||
// change isAdmin
|
||||
switch (user.isAdmin) {
|
||||
case null:
|
||||
if (isAdmin === true) {
|
||||
user.isAdmin = new Date()
|
||||
} else {
|
||||
throw new Error('User is already a usual user!')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (isAdmin === false) {
|
||||
user.isAdmin = null
|
||||
} else {
|
||||
throw new Error('User is already admin!')
|
||||
}
|
||||
break
|
||||
}
|
||||
await user.save()
|
||||
const newUser = await dbUser.findOne({ id: userId })
|
||||
return newUser ? newUser.isAdmin : null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteUser(
|
||||
@ -175,7 +229,6 @@ export class AdminResolver {
|
||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Decimal[]> {
|
||||
logger.trace('adminCreateContribution...')
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
@ -516,6 +569,39 @@ export class AdminResolver {
|
||||
maxPerCycle,
|
||||
}: ContributionLinkArgs,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (!name) {
|
||||
logger.error(`The name must be initialized!`)
|
||||
throw new Error(`The name must be initialized!`)
|
||||
}
|
||||
if (
|
||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
||||
name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'name' with a length of ${name.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_NAME_MIN_CHARS} and max=${CONTRIBUTIONLINK_NAME_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!memo) {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
}
|
||||
if (
|
||||
memo.length < CONTRIBUTIONLINK_MEMO_MIN_CHARS ||
|
||||
memo.length > CONTRIBUTIONLINK_MEMO_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_MEMO_MIN_CHARS} and max=${CONTRIBUTIONLINK_MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!amount) {
|
||||
logger.error(`The amount must be initialized!`)
|
||||
throw new Error('The amount must be initialized!')
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
}
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
@ -528,6 +614,7 @@ export class AdminResolver {
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`createContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
|
||||
@ -557,6 +644,7 @@ export class AdminResolver {
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
}
|
||||
await contributionLink.softRemove()
|
||||
logger.debug(`deleteContributionLink successful!`)
|
||||
const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true })
|
||||
return newContributionLink ? newContributionLink.deletedAt : null
|
||||
}
|
||||
@ -591,6 +679,7 @@ export class AdminResolver {
|
||||
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
|
||||
dbContributionLink.maxPerCycle = maxPerCycle
|
||||
await dbContributionLink.save()
|
||||
logger.debug(`updateContributionLink successful!`)
|
||||
return new ContributionLink(dbContributionLink)
|
||||
}
|
||||
}
|
||||
@ -684,6 +773,27 @@ export const isContributionValid = (
|
||||
return true
|
||||
}
|
||||
|
||||
const isStartEndDateValid = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined,
|
||||
): void => {
|
||||
if (!startDate) {
|
||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||
}
|
||||
|
||||
// check if endDate is before startDate
|
||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||
}
|
||||
}
|
||||
|
||||
const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
|
||||
@ -11,6 +11,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
@ -29,6 +30,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/mailer/sendResetPasswordEmail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
@ -156,14 +164,33 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('email already exists', () => {
|
||||
it('throws and logs an error', async () => {
|
||||
const mutation = await mutate({ mutation: createUser, variables })
|
||||
let mutation: User
|
||||
beforeAll(async () => {
|
||||
mutation = await mutate({ mutation: createUser, variables })
|
||||
})
|
||||
|
||||
it('logs an info', async () => {
|
||||
expect(logger.info).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
|
||||
it('sends an account multi registration email', () => {
|
||||
expect(sendAccountMultiRegistrationEmail).toBeCalledWith({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('results with partly faked user with random "id"', async () => {
|
||||
expect(mutation).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User already exists.')],
|
||||
data: {
|
||||
createUser: {
|
||||
id: expect.any(Number),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { getConnection } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
import { encode } from '@/auth/JWT'
|
||||
@ -18,6 +19,7 @@ import { OptInType } from '@enum/OptInType'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
@ -328,10 +330,35 @@ export class UserResolver {
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||
|
||||
if (userFound) {
|
||||
logger.error('User already exists with this email=' + email)
|
||||
logger.info('User already exists with this email=' + email)
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
throw new Error(`User already exists.`)
|
||||
|
||||
const user = new User(communityDbUser)
|
||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
logger.debug('partly faked user=' + user)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Email not send!`)
|
||||
}
|
||||
logger.info('createUser() faked and send multi registration mail...')
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const passphrase = PassphraseGenerate()
|
||||
@ -417,6 +444,7 @@ export class UserResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.info('createUser() successful...')
|
||||
|
||||
return new User(dbUser)
|
||||
}
|
||||
|
||||
|
||||
31
backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendAccountMultiRegistrationEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendAccountMultiRegistrationEmail({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Peter Lustig <peter@lustig.de>`,
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text:
|
||||
expect.stringContaining('Hallo Peter Lustig') &&
|
||||
expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
|
||||
expect.stringContaining('https://gradido.net/de/contact/'),
|
||||
})
|
||||
})
|
||||
})
|
||||
18
backend/src/mailer/sendAccountMultiRegistrationEmail.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { accountMultiRegistration } from './text/accountMultiRegistration'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export const sendAccountMultiRegistrationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
subject: accountMultiRegistration.de.subject,
|
||||
text: accountMultiRegistration.de.text({
|
||||
...data,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@ -31,6 +31,7 @@ describe('sendEMail', () => {
|
||||
beforeEach(async () => {
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -50,6 +51,7 @@ describe('sendEMail', () => {
|
||||
CONFIG.EMAIL = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
@ -72,6 +74,7 @@ describe('sendEMail', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
|
||||
@ -5,10 +5,15 @@ import CONFIG from '@/config'
|
||||
|
||||
export const sendEMail = async (emailDef: {
|
||||
to: string
|
||||
cc?: string
|
||||
subject: string
|
||||
text: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
|
||||
logger.info(
|
||||
`send Email: to=${emailDef.to}` +
|
||||
(emailDef.cc ? `, cc=${emailDef.cc}` : '') +
|
||||
`, subject=${emailDef.subject}, text=${emailDef.text}`,
|
||||
)
|
||||
|
||||
if (!CONFIG.EMAIL) {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
|
||||
25
backend/src/mailer/text/accountMultiRegistration.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const accountMultiRegistration = {
|
||||
de: {
|
||||
subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
|
||||
text: (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
resendLink: string
|
||||
}): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.
|
||||
Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.
|
||||
|
||||
Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:
|
||||
${data.resendLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:
|
||||
https://gradido.net/de/contact/
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -98,6 +98,12 @@ export const confirmContribution = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const setUserRole = gql`
|
||||
mutation ($userId: Int!, $isAdmin: Boolean!) {
|
||||
setUserRole(userId: $userId, isAdmin: $isAdmin)
|
||||
}
|
||||
`
|
||||
|
||||
export const deleteUser = gql`
|
||||
mutation ($userId: Int!) {
|
||||
deleteUser(userId: $userId)
|
||||
|
||||
@ -110,6 +110,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v7.2022-06-15
|
||||
BACKEND_CONFIG_VERSION=v8.2022-06-20
|
||||
|
||||
JWT_EXPIRES_IN=30m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
173
docu/Concepts/BusinessRequirements/UC_Send_Contribution.md
Normal file
@ -0,0 +1,173 @@
|
||||
# GDD-Creation per Link/QR-Code
|
||||
|
||||
Die Idee besteht darin, dass ein Administrator eine Contribution mit all seinen Attributen und Regeln im System erfasst. Dabei kann er unter anderem festlegen, ob für diese ein Link oder ein QR-Code generiert und über andere Medien wie Email oder Messenger versendet werden kann. Der Empfänger kann diesen Link bzw QR-Code dann über die Gradido-Anwendung einlösen und bekommt dann den Betrag der Contribution als Schöpfung auf seinem Konto gutgeschrieben.
|
||||
|
||||
## Logischer Ablauf
|
||||
|
||||
Der logische Ablauf für das Szenario "Activity-Confirmation and booking of Creations " wird in der nachfolgenden Grafik dargestellt. Dabei wird links das Szenario der "interactive Confirmation and booking of Creations" und rechts "automatic Confirmation and booking of Creations" dargestellt. Ziel dieser Grafik ist neben der logischen Ablaufsübersicht auch die Gemeinsamkeiten und Unterschiede der beiden Szenarien herauszuarbeiten.
|
||||
|
||||

|
||||
|
||||
Das Szenario der *interaktiven Aktivitäten-Bestätigung* ist derzeit noch in den zwei Systemen EloPage und Gradido enthalten - markiert als IST-Prozess - und wird zukünftig dann nur noch innerhalb Gradido ablaufen - markiert als SOLL-Prozess. Mit der Ablösung von EloPage und der vollständigen Migration nach Gradido erfolgt gleichzeitig eine Migration der Datenbank-Tabelle "admin_pending-creations" nach "Contributions". Unterhalb der gestrichelten Linie sind die beiden Szenarien dann in der Ablauflogik vollständig gleich.
|
||||
|
||||
## Dialoge
|
||||
|
||||
Für die Erfassung, Suche und Anzeige der Contributions und deren Gliederung in Kategorien wird es dazu im Admin-Bereich zusätzliche Funktionen und Dialoge geben.
|
||||
|
||||
### Übersicht - Dialog
|
||||
|
||||
In der Admin-Übersicht wird es zusätzliche Navigations- bzw. Menüpunkte geben, über die der Admin die gewünschte Funktionalität und die zugehörigen Dialoge öffnen kann.
|
||||
|
||||

|
||||
|
||||
### Contribution erfassen - Dialog
|
||||
|
||||
Bei der Erfassung einer Contribution wird die Kategorie, ein Name, eine Beschreibung der Contribution und der Betrag eingegeben.
|
||||
|
||||
Der Gültigkeitsstart wird als Default mit dem aktuellen Erfassungszeitpunkt vorbelegt, wobei das Gültigkeitsende leer bleibt und damit als endlos gültig definiert wird. Mit Eingabe eines Start- und/oder Endezeitpunktes kann aber ein konkreter Gültigkeitszeitraum erfasst werden.
|
||||
|
||||
Wie häufig ein User für diese Contribution eine Schöpfung gutgeschrieben bekommen kann, wird über die Auswahl eines Zyklus - stündlich, 2-stündlich, 4-stündlich, etc. - und innerhalb dieses Zyklus eine Anzahl an Wiederholungen definiert. Voreinstellung sind 1x täglich.
|
||||
|
||||

|
||||
|
||||
Ob die Contribution über einen versendeten Link bzw. QR-Code geschöpft werden kann, wird mittels der Auswahl "Versenden möglich als" bestimmt.
|
||||
|
||||

|
||||
|
||||
Für die Schöpfung der Contribution können weitere Regeln definiert werden:
|
||||
|
||||
* Gesamt - max. Anzahl Schöpfungen: bestimmt die maximale Anzahl der möglichen Schöpfungen über alle User dieser Community. Sobald diese Anzahl an Schöpfungen erreicht ist, werden alle weiteren eingehenden Schöpfungsanfragen für diese Contribution -egal ob per Links, per QR-Code oder User-Online-Erfassung mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* pro User
|
||||
* max schöpfbarer Betrag pro Monat: mit diesem definierbaren Betrag kann vordefiniert werden, wieviel Gradido ein User innerhalb eines Abrechnungsmonats maximal durch diese Contribution schöpfen kann. Ist diese Summer erreicht werden weiter eingehende Schöpfungsanfragen - egal ob per Link, per QR-Code oder online - mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* max. Kontostand vor Schöpfung: mit diesem definierbaren Betrag kann festgelegt werden, dass bevor für diese Contribution eine Schöpfung für den user erfolgt, eine Prüfung auf den aktuellen Kontostand erfolgt. Sobald der Kontostand höher als der vorgegebene Betrag ist, wird die eingehende Schöpfungsanfrage, ob per Link, per QR-Code oder online, mit einer entsprechend aussagekräftigen Fehlermeldung abgelehnt.
|
||||
* min. Abstand zw. erneuter Schöpfung: es kann ein zeitlicher Abstand in Stunden definiert werden, der angibt wieviel Stunden seit der letzten erfolgten Schöpfung vergehen müssen, bevor eine erneute Schöpfungsanfrage, ob per Link, per QR-Code oder online angenommen und durchgeführt werden darf. Ist bei einer erneuten Schöpfungsanfrage der zeitliche Abstand noch nicht erreicht, dann wird mit einer entsprechend aussagekräftigen Fehlermeldung abgebrochen.
|
||||
|
||||

|
||||
|
||||
### Ausbaustufe-1:
|
||||
|
||||
Die Ausbaustufe-1 wird gezielt auf die Anforderungen der "Dokumenta" im Juni 2022 abgestimmt.
|
||||
|
||||
#### Contribution-Erfassungsdialog (Adminbereich)
|
||||
|
||||
Es werden folgende Anforderungen an den Erfassungsdialog einer Contribution gestellt:
|
||||
|
||||
| Attribut | Beschreibung |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Name | Name der Contribution als Bezeichnung, nach dem später auch eine Suche erfolgen kann |
|
||||
| Beschreibung | Beschreibung der Contribution, die mit der Schöpfung als Memo in die Transaktion übernommen wird |
|
||||
| Betrag | der Betrag, der mit Einlösen der Contribution geschöpft wird |
|
||||
| GültigVon | - das Datum, ab wann die Contribution gültig und damit einlösbar ist<br />- es wird die Uhrzeit 00:00:00 angenommen |
|
||||
| GültigBis | - das Datum, wie lange die Contribution gültig und damit einlösbar ist<br />- es wird die Uhrzeit 23:59:59 angenommen |
|
||||
| Zyklus | - Angabe wie häufig eine Contribution gutgeschrieben werden kann<br />- als Auswahlliste (Combobox) geplant, aber für diese Ausbaustufe nur mit dem Wert "kein Wiederholungszyklus" vorbelegt |
|
||||
| Wiederholungen | - Anzahl an Wiederholungen pro Zyklus<br />- für diese Ausbaustufe wird der Wert "1" vorbelegt -> somit gilt 1 x pro User |
|
||||
| VersendenMöglich | - hier wird "als Link / QR-Code" voreingestellt |
|
||||
| alle weiteren Attribute | - entfallen für diese Ausbaustufe<br />- die GUI-Komponenten können optional schon im Dialog eingebaut und angezeigt werden<br />- diese GUI-Komponenten müssen wenn sichtbar disabled sein und dürfen damit keine Eingaben entgegen nehmen |
|
||||
|
||||
|
||||
#### Ablauflogik
|
||||
|
||||
Für die Ausbaustufe-1 wird gemäß der Beschreibung aus dem Kapitel "Logischer Ablauf" nur die "automatic Confirmation and booking of Creations" umgesetzt. Die interaktive Variante - sprich Ablösung des EloPage Prozesses - mit "interactive Confirmation and booking of Creations" bleibt für eine spätere Ausbaustufe aussen vor.
|
||||
|
||||
Das Regelwerk in der Businesslogik wird gemäß der reduzierten Contribution-Attribute aus dem Erfassungsdialog, den vordefinierten Initialwerten und der daraus resultierenden Variantenvielfalt vereinfacht.
|
||||
|
||||
|
||||
#### Kriterien "Dokumenta"
|
||||
|
||||
* Es soll eine "Dokumenta"-Contribution im Admin-Bereich erfassbar sein und in der Datenbank als ContributionLink gespeichert werden.
|
||||
* Es wird für die Gesamtlaufzeit der "Dokumenta" genau ein Contribution benötigt
|
||||
* Die "Dokumenta"-Contribution kann von einem User maximal 1x aktiviert werden
|
||||
* Ein User kann mit diesem Link nur die Menge an GDDs schöpfen, die in der Contribution als "Betrag" festgelegt ist
|
||||
* Die "Dokumenta"-Contribution kann als Link / QR-Code erzeugt, angezeigt und in die Zwischenablage kopiert werden
|
||||
* Jeder beliebige User kann den Link / QR-Code aktivieren
|
||||
* der Link führt auf eine Gradido-Seite, wo der User sich anmelden oder registrieren kann
|
||||
* mit erfolgreichem Login bzw. Registrierung wird der automatische Bestätigungs- und Schöpfungsprozess getriggert
|
||||
* es erfolgt eine Überprüfung der definierten Contribution-Regeln für den angemeldeten User:
|
||||
* Gültigkeit: liegt die Aktivierung im Gültigkeitszeitraum der Contribution
|
||||
* Zyklus und WIederholungen: bei einem Zyklus-Wert = "kein Zyklus" und einem Wiederholungswert = 1 darf der User den Betrag dieser Contribution nur einmal insgesamt schöpfen
|
||||
* max. schöpfbarer Gradido-Betrag pro Monat: wenn der Betrag der Contribution plus der Betrag, den der User in diesem Monat schon geschöpft hat den maximal schöpfbaren Betrag pro Monat von 1000 GDD übersteigt, dann wird die Schöpfung dieser Contribution abgelehnt
|
||||
* mit erfolgreich durchlaufenen Regelprüfungen wird ein "besätigter" aber "noch nicht gebuchten" Eintrag in der "Contributions"-Tabelle erzeugt
|
||||
* ein "bestätigter" aber "noch nicht gebuchter" "Contributions"-Eintrag stößt eine Schöpfungstransaktion für den User an
|
||||
* es erfolgt eine übliche Schöpfungstransaktion nach der Bestätigung der Contribution
|
||||
* die Schöpfungstransaktion schreibt den Betrag der Contribution dem Kontostand des Users gut
|
||||
|
||||
|
||||
## Datenbank-Modell
|
||||
|
||||
### Ausgangsmodell
|
||||
|
||||
Das nachfolgende Bild zeigt das Datenmodell vor der Einführung und Migration auf Contributions.
|
||||
|
||||

|
||||
|
||||
### Datenbank-Änderungen
|
||||
|
||||
Die Datenbank wird in ihrer vollständigen Ausprägung trotz Ausbaustufe-1 wie folgt beschrieben umgesetzt.
|
||||
|
||||
#### neue Tabellen
|
||||
|
||||
##### contribution_links - Tabelle
|
||||
|
||||
| Name | Typ | Nullable | Default | Kommentar |
|
||||
| ------------------------------- | ------------ | :------: | :------------: | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | INT UNSIGNED | NOT NULL | auto increment | PrimaryKey |
|
||||
| name | varchar(100) | NOT NULL | | unique Name |
|
||||
| description | varchar(255) | | | |
|
||||
| valid_from | DATETIME | NOT NULL | NOW | |
|
||||
| valid_to | DATETIME | | NULL | |
|
||||
| amount | DECIMAL | NOT NULL | | |
|
||||
| cycle | ENUM | NOT NULL | ONCE | ONCE, HOUR, 2HOUR, 4HOUR, 8HOUR, HALFDAY, DAY, 2DAYS, 3DAYS, 4DAYS, 5DAYS, 6DAYS, WEEK, 2WEEKS, MONTH, 2MONTH, QUARTER, HALFYEAR, YEAR |
|
||||
| max_per_cycle | INT UNSIGNED | NOT NULL | 1 | |
|
||||
| max_amount_per_month | DECIMAL | | NULL | |
|
||||
| total_max_count_of_contribution | INT UNSIGNED | | NULL | |
|
||||
| max_account_balance | DECIMAL | | NULL | |
|
||||
| min_gap_hours | INT UNSIGNED | | NULL | |
|
||||
| created_at | DATETIME | | NOW | |
|
||||
| deleted_at | DATETIMEBOOL | | NULL | |
|
||||
| code | varchar(24) | | NULL | |
|
||||
| link_enabled | BOOL | | NULL | |
|
||||
|
||||
##### contributions -Tabelle
|
||||
|
||||
| Name | Typ | Nullable | Default | Kommentar |
|
||||
| --------------------- | ------------ | -------- | -------------- | -------------------------------------------------------------------------------- |
|
||||
| id | INT UNSIGNED | NOT NULL | auto increment | PrimaryKey |
|
||||
| name | varchar(100) | NOT NULL | | short Naming of activity |
|
||||
| memo | varchar(255) | NOT NULL | | full and detailed description of activities |
|
||||
| amount | DECIMAL | NOT NULL | | the amount of GDD for this activity |
|
||||
| contribution_date | DATETIME | | NULL | the date/month, when the contribution was realized by the user |
|
||||
| user_id | INT UNSIGNED | NOT NULL | | the user, who wants to get GDD for his activity |
|
||||
| created_at | DATETIME | NOT NULL | NOW | the date, when this entry was captured and stored in database |
|
||||
| contribution_links_id | INT UNSIGNED | | NULL | contribution, on which this activity base on |
|
||||
| moderator_id | INT UNSIGNED | | NULL | userID of Moderator/Admin, who captured the contribution |
|
||||
| confirmed_by | INT UNSIGNED | | NULL | userID of Moderator/Admin, who confirms the contribution |
|
||||
| confirmed_at | DATETIME | | NULL | date, when moderator has confirmed the contribution |
|
||||
| booked_at | DATETIME | | NULL | date, when the system has booked the amount of the activity on the users account |
|
||||
| deleted_at | DATETIME | | NULL | soft delete |
|
||||
|
||||
#### zu migrierende Tabellen
|
||||
|
||||
##### Tabelle admin_pending_creations
|
||||
|
||||
Diese Tabelle wird im Rahmen dieses UseCase migriert in die neue Tabelle contributions...
|
||||
|
||||
| Quell-Spalte | Migration | Ziel-Spalte | Beschreibung |
|
||||
| ------------ | --------- | --------------------- | ---------------------------------------------------------- |
|
||||
| id | keine | id | auto inkrement des PK |
|
||||
| user_id | copy | user_id | |
|
||||
| created | copy | created_at | |
|
||||
| date | copy | activity_date | |
|
||||
| memo | copy | memo | |
|
||||
| amount | copy | amount | |
|
||||
| moderator | copy | moderator_id | |
|
||||
| | | name | neu mit ContributionsLinks |
|
||||
| | | contribution_links_id | neu mit ContributionsLinks |
|
||||
| | | confirmed_at | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
| | | confirmed_by | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
| | | booked_at | neu mit Erfassung der Contributions von Elopage in Gradido |
|
||||
|
||||
...und kann nach Übernahme der Daten in die neue Tabelle gelöscht werden oder es erfolgen die Änderungen sofort auf der Ursprungstabelle.
|
||||
|
||||
### Zielmodell
|
||||
|
||||

|
||||
@ -0,0 +1,368 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="-Bvenr9G4hMm7q4_ZwMA" name="Seite-1">
|
||||
<mxGraphModel dx="3755" dy="1067" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="6" value="EloPage" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="80" width="1080" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="interactive Confirmation and booking of Creations" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=28;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="10" width="1080" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="automatic Confirmation and booking of Creations" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=28;fontStyle=1" parent="1" vertex="1">
|
||||
<mxGeometry x="1200" y="10" width="1080" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="endArrow=none;html=1;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="1650" as="sourcePoint"/>
|
||||
<mxPoint x="1160" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="45" value="" style="edgeStyle=none;html=1;fontSize=14;" parent="1" source="5" target="44" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="User erfasst Activity" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="80" y="140" width="240" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="210" width="1080" height="150" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=24;" parent="1" source="8" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Moderator überträgt&nbsp; offene User-Activity aus EloPage" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="80" y="236" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="admin_pending_creations" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="261" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="10" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="261" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="440" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="440" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="IST-Prozess" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" parent="1" vertex="1">
|
||||
<mxGeometry y="400" width="210" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="SOLL-Prozess" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=24;" parent="1" vertex="1">
|
||||
<mxGeometry y="450" width="210" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="520" width="1080" height="1080" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="690" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="19" target="18" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="690" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="20" target="18" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="User erfasst seine Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="690" width="240" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="23" target="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="Moderator sucht unbestätigte <br>Contributions" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="790" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="25" target="23" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="contributions<br style="font-size: 24px"><font style="font-size: 20px">confirmed_at == NULL</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="795" width="380" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="28" target="31" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="" style="edgeStyle=none;html=1;fontSize=14;" parent="1" source="28" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="Moderator bestätigt Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="920" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="<font style="font-size: 23px"><span style="font-size: 24px">contributions</span><br></font><div style="text-align: left ; font-size: 20px"><font style="font-size: 20px">confirmed_at = NOW</font></div><span style="line-height: 0.8 ; font-size: 20px"><div style="text-align: left"><font style="font-size: 20px">confirmed_by = Moderator's userID</font></div></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="930" width="380" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" value="" style="edgeStyle=none;html=1;startArrow=none;" parent="1" source="50" target="48" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="53" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="49" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3333" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="51" value="" style="edgeStyle=none;html=1;" parent="1" source="34" target="50" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="&nbsp;lese Transaktionen des Users zu bestätigter<br>Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1090" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" value="" style="edgeStyle=none;html=1;fontSize=12;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="35" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="35" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1080" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="39" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="780" y="1110" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="41" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;" parent="1" source="39" target="40" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="1110" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="42" style="edgeStyle=none;html=1;fontSize=12;" parent="1" source="40" target="34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="40" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1150" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" style="edgeStyle=none;html=1;entryX=0.75;entryY=0;entryDx=0;entryDy=0;fontSize=14;dashed=1;startArrow=classic;startFill=1;exitX=0;exitY=1;exitDx=0;exitDy=0;" parent="1" source="44" target="8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="44" value="Aktivitäten-Liste als Chat" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;fillColor=#f5f5f5;strokeColor=#666666;gradientColor=#b3b3b3;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="130" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;strokeWidth=3;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="1040" as="sourcePoint"/>
|
||||
<mxPoint x="1160" y="1040" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="57" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="48" target="55" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="60" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="48" target="59" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="48" value="erzeuge Schöpfungstransaktion<br>aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1330" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="50" value="<br>Schöpfungsregeln<br>&nbsp;erfüllt?" style="rhombus;whiteSpace=wrap;html=1;fontSize=16;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="110" y="1210" width="200" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="52" value="" style="edgeStyle=none;html=1;endArrow=none;" parent="1" source="34" target="50" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="210" y="1230" as="sourcePoint"/>
|
||||
<mxPoint x="210" y="1410" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="55" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1340" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="58" value="contributions<br style="font-size: 24px"><span style="text-align: left"><font style="font-size: 20px">booked_at = NOW</font></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="400" y="1460" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="61" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="59" target="58" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="59" value="aktualisiere <br>&nbsp;gebuchte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="1450" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="62" value="Gradido" style="rounded=0;whiteSpace=wrap;html=1;fontSize=24;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=center;" parent="1" vertex="1">
|
||||
<mxGeometry x="1200" y="80" width="980" height="1520" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="117" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="63" target="67" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="63" value="contribution_links<br style="font-size: 24px"><span style="font-size: 20px">id = X<br>code = X-link<br></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="670" width="380" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="121" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="65" target="71" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="65" value="users<br style="font-size: 24px"><font style="font-size: 20px">ID=Y</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="930" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="128" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="67" target="127" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="67" value="lese Contribution zu aktiviertem Link" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="690" width="240" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="120" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="69" target="71" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="122" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="69" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="69" value="erzeuge aus ContributionLink zu angemeldetem User eine bestätigte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="907.5" width="240" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="71" value="contributions<br style="font-size: 24px"><font style="font-size: 20px">confirmed_at = NOW, contribution_links_id=X, user_id=Y</font>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="917.5" width="380" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="72" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1490" y="855" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="76" value="" style="edgeStyle=none;html=1;startArrow=none;" parent="1" source="91" target="90" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="77" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="76" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3333" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="78" value="" style="edgeStyle=none;html=1;" parent="1" source="79" target="91" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="79" value="&nbsp;lese Transaktionen des Users zu bestätigter<br>Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1090" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="80" value="" style="edgeStyle=none;html=1;fontSize=12;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="81" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="81" value="contributions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1080" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="82" value="" style="edgeStyle=none;html=1;fontSize=24;" parent="1" source="84" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1940" y="1110" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="83" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=12;" parent="1" source="84" target="86" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="84" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="1110" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="85" style="edgeStyle=none;html=1;fontSize=12;" parent="1" source="86" target="79" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="86" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1150" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="87" value="" style="endArrow=none;dashed=1;html=1;fontSize=24;strokeWidth=3;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="1160" y="1040" as="sourcePoint"/>
|
||||
<mxPoint x="2320" y="1040" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="88" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="90" target="93" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="90" target="96" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="90" value="erzeuge Schöpfungstransaktion<br>aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1330" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="91" value="<br>Schöpfungsregeln<br>&nbsp;erfüllt?" style="rhombus;whiteSpace=wrap;html=1;fontSize=16;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="1270" y="1210" width="200" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="92" value="" style="edgeStyle=none;html=1;endArrow=none;" parent="1" source="79" target="91" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1370" y="1230" as="sourcePoint"/>
|
||||
<mxPoint x="1370" y="1410" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="93" value="transactions" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1340" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="94" value="contributions<br style="font-size: 24px"><span style="text-align: left"><font style="font-size: 20px">booked_at = NOW</font></span>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="1460" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="95" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="96" target="94" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="96" value="aktualisiere <br>gebuchte Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="1450" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="99" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="97" target="98" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="97" value="Moderator erfasst Contribution für <br>"automatic Confirmation"" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="117.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="98" value="contribution_links" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="127.5" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="101" value="Moderator erzeugt Link/QR-Code aus Contribution" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="213.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="103" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="102" target="101" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="102" value="contribution_links" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1560" y="223.5" width="380" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="110" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" source="104" target="109" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="104" value="Moderator <br>verbreitet / versendet<br>&nbsp;erzeugten Link/QR-Code" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="312.5" width="240" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="112" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="109" target="111" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1912.7716129809019" y="520.971840668889" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="109" value="veröffentlichter <br>Link / QR-Code für<br>eine Contribution" style="ellipse;whiteSpace=wrap;html=1;fontSize=20;rounded=1;fillColor=#d0cee2;strokeColor=#56517e;" parent="1" vertex="1">
|
||||
<mxGeometry x="2010" y="410" width="310" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="118" value="" style="edgeStyle=none;html=1;fontSize=16;" parent="1" target="113" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1370" y="570" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="111" value="User aktiviert <br>Link / QR-Code" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="510" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="115" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="113" target="114" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="116" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="113" target="67" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="113" value="User führt <br>Login / Register aus" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="597.5" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="114" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="1990" y="592.5" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="123" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;" parent="1" source="124" target="125" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="126" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="124" target="20" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="124" value="User führt <br>Login / Register aus" style="rounded=1;whiteSpace=wrap;html=1;fontSize=20;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
|
||||
<mxGeometry x="90" y="597.5" width="240" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="125" value="users" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fontSize=24;size=0.05263157894736842;" parent="1" vertex="1">
|
||||
<mxGeometry x="880" y="592.5" width="170" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="129" value="" style="edgeStyle=none;html=1;fontSize=20;strokeWidth=1;" parent="1" source="127" target="69" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="130" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=20;" parent="129" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3467" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="127" value="Contribution <br>und Regel <br>valide?" style="rhombus;whiteSpace=wrap;html=1;fontSize=20;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="1250" y="770" width="240" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 92 KiB |
@ -1,9 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-carousel :interval="13000">
|
||||
<b-carousel-slide img-src="/img/template/Foto_01_2400_small.jpg"></b-carousel-slide>
|
||||
<b-carousel-slide img-src="/img/template/Foto_02_2400_small.jpg"></b-carousel-slide>
|
||||
<b-carousel-slide img-src="/img/template/Foto_03_2400_small.jpg"></b-carousel-slide>
|
||||
<b-carousel-slide img-src="/img/template/Foto_01_2400_small.jpg">
|
||||
<div class="caption-first-text">{{ $t('auth.left.gratitude') }}</div>
|
||||
<div class="caption-second-text">{{ $t('auth.left.oneGratitude') }}</div>
|
||||
</b-carousel-slide>
|
||||
<b-carousel-slide img-src="/img/template/Foto_02_2400_small.jpg">
|
||||
<div class="caption-first-text">{{ $t('auth.left.dignity') }}</div>
|
||||
<div class="caption-second-text">{{ $t('auth.left.oneDignity') }}</div>
|
||||
</b-carousel-slide>
|
||||
<b-carousel-slide img-src="/img/template/Foto_03_2400_small.jpg">
|
||||
<div class="caption-first-text">{{ $t('auth.left.donation') }}</div>
|
||||
<div class="caption-second-text">{{ $t('auth.left.oneDonation') }}</div>
|
||||
</b-carousel-slide>
|
||||
</b-carousel>
|
||||
</div>
|
||||
</template>
|
||||
@ -15,6 +24,21 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.carousel-caption {
|
||||
color: #fff;
|
||||
top: 317px;
|
||||
text-shadow: 2px 2px 8px #000000;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.caption-first-text {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.caption-second-text {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
height: 110%;
|
||||
|
||||
@ -13,9 +13,6 @@
|
||||
<auth-carousel class="carousel" />
|
||||
</div>
|
||||
<div class="bg-txt-box position-relative d-none d-lg-block text-center align-self-center">
|
||||
<div class="h0 text-white">{{ $t('auth.left.gratitude') }}</div>
|
||||
<div class="h1 text-white">{{ $t('auth.left.newCurrency') }}</div>
|
||||
<div class="h2 text-white">{{ $t('auth.left.oneAnotherNature') }}</div>
|
||||
<b-link :href="`https://gradido.net/${$i18n.locale}`" target="_blank">
|
||||
<b-button variant="gradido">
|
||||
{{ $t('auth.left.learnMore') }}
|
||||
@ -150,20 +147,10 @@ export default {
|
||||
}
|
||||
|
||||
.bg-txt-box {
|
||||
margin-top: 317px;
|
||||
margin-top: 520px;
|
||||
text-shadow: 2px 2px 8px #000000;
|
||||
max-width: 733px;
|
||||
}
|
||||
.bg-txt-box > .h0 {
|
||||
font-size: 4em;
|
||||
text-shadow: -2px -2px -8px #e4a907;
|
||||
}
|
||||
|
||||
.bg-txt-box .h1,
|
||||
.bg-txt-box .h2 {
|
||||
font-size: 1.5em;
|
||||
text-shadow: -2px -2px -8px #e4a907;
|
||||
}
|
||||
|
||||
.bg-img {
|
||||
border-radius: 0% 50% 70% 0% / 50% 70% 70% 50%;
|
||||
|
||||
@ -6,12 +6,16 @@
|
||||
"advanced-calculation": "Vorausberechnung",
|
||||
"auth": {
|
||||
"left": {
|
||||
"dignity": "Würde",
|
||||
"donation": "Gabe",
|
||||
"gratitude": "Dankbarkeit",
|
||||
"hasAccount": "Du hast schon einen Account?",
|
||||
"hereLogin": "Hier Anmelden",
|
||||
"learnMore": "Erfahre mehr …",
|
||||
"newCurrency": "Die neue Währung",
|
||||
"oneAnotherNature": "FÜR EINANDER, FÜR ALLE, FÜR DIE NATUR"
|
||||
"oneDignity": "Wir schenken einander und danken mit Gradido.",
|
||||
"oneDonation": "Du bist ein Geschenk für die Gemeinschaft. 1000 Dank, weil du bei uns bist.",
|
||||
"oneGratitude": "Die neue Währung. Für einander, für alle Menschen, für die Natur."
|
||||
},
|
||||
"navbar": {
|
||||
"aboutGradido": "Über Gradido"
|
||||
@ -41,7 +45,7 @@
|
||||
"Starting_block_decay": "Startblock Vergänglichkeit",
|
||||
"total": "Gesamt",
|
||||
"types": {
|
||||
"created": "Geschöpft",
|
||||
"creation": "Geschöpft",
|
||||
"noDecay": "Keine Vergänglichkeit",
|
||||
"receive": "Empfangen",
|
||||
"send": "Gesendet"
|
||||
@ -57,8 +61,7 @@
|
||||
"no-transactionlist": "Es gab leider einen Fehler. Es wurden keine Transaktionen vom Server übermittelt.",
|
||||
"no-user": "Kein Benutzer mit diesen Anmeldedaten.",
|
||||
"session-expired": "Die Sitzung wurde aus Sicherheitsgründen beendet.",
|
||||
"unknown-error": "Unbekanter Fehler: ",
|
||||
"user-already-exists": "Ein Benutzer mit diesen Daten existiert bereits."
|
||||
"unknown-error": "Unbekannter Fehler: "
|
||||
},
|
||||
"followUs": "folge uns:",
|
||||
"footer": {
|
||||
@ -261,7 +264,7 @@
|
||||
"minutes": "Minuten",
|
||||
"months": "Monate",
|
||||
"seconds": "Sekunden",
|
||||
"year": "Jahre"
|
||||
"years": "Jahr"
|
||||
},
|
||||
"transaction": {
|
||||
"gdd-text": "Gradido Transaktionen",
|
||||
|
||||
@ -6,12 +6,16 @@
|
||||
"advanced-calculation": "Advanced calculation",
|
||||
"auth": {
|
||||
"left": {
|
||||
"dignity": "Dignity",
|
||||
"donation": "Donation",
|
||||
"gratitude": "Gratitude",
|
||||
"hasAccount": "You already have an account?",
|
||||
"hereLogin": "Log in here",
|
||||
"learnMore": "Learn more …",
|
||||
"newCurrency": "The new currency",
|
||||
"oneAnotherNature": "FOR EACH OTHER, FOR ALL, FOR NATURE"
|
||||
"oneDignity": "We gift to each other and give thanks with Gradido.",
|
||||
"oneDonation": "You are a gift for the community. 1000 thanks because you are with us.",
|
||||
"oneGratitude": "The new currency. For each other, for all people, for nature."
|
||||
},
|
||||
"navbar": {
|
||||
"aboutGradido": "About Gradido"
|
||||
@ -41,7 +45,7 @@
|
||||
"Starting_block_decay": "Starting Block Decay",
|
||||
"total": "Total",
|
||||
"types": {
|
||||
"created": "Created",
|
||||
"creation": "Created",
|
||||
"noDecay": "No Decay",
|
||||
"receive": "Received",
|
||||
"send": "Sent"
|
||||
@ -57,8 +61,7 @@
|
||||
"no-transactionlist": "Unfortunately, there was an error. No transactions have been sent from the server.",
|
||||
"no-user": "No user with this credentials.",
|
||||
"session-expired": "The session was closed for security reasons.",
|
||||
"unknown-error": "Unknown error: ",
|
||||
"user-already-exists": "A user with this data already exists."
|
||||
"unknown-error": "Unknown error: "
|
||||
},
|
||||
"followUs": "follow us:",
|
||||
"footer": {
|
||||
@ -261,7 +264,7 @@
|
||||
"minutes": "Minutes",
|
||||
"months": "Months",
|
||||
"seconds": "Seconds",
|
||||
"year": "Years"
|
||||
"years": "Year"
|
||||
},
|
||||
"transaction": {
|
||||
"gdd-text": "Gradido Transactions",
|
||||
|
||||
@ -139,24 +139,6 @@ describe('Register', () => {
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('server sends back error "User already exists."', () => {
|
||||
beforeEach(async () => {
|
||||
await createError('GraphQL error: User already exists.')
|
||||
})
|
||||
|
||||
it('shows no error message on the page', () => {
|
||||
// don't show any error on the page! against boots
|
||||
expect(wrapper.vm.showPageMessage).toBe(false)
|
||||
expect(wrapper.find('.test-message-headline').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-subtitle').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.user-already-exists')
|
||||
})
|
||||
})
|
||||
|
||||
describe('server sends back error "Unknown error"', () => {
|
||||
beforeEach(async () => {
|
||||
await createError(' – Unknown error.')
|
||||
|
||||
@ -122,9 +122,6 @@ export default {
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null
|
||||
},
|
||||
commitStorePublisherId(val) {
|
||||
this.$store.commit('publisherId', val)
|
||||
},
|
||||
async onSubmit() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
@ -142,16 +139,7 @@ export default {
|
||||
this.showPageMessage = true
|
||||
})
|
||||
.catch((error) => {
|
||||
let errorMessage
|
||||
switch (error.message) {
|
||||
case 'GraphQL error: User already exists.':
|
||||
errorMessage = this.$t('error.user-already-exists')
|
||||
break
|
||||
default:
|
||||
errorMessage = this.$t('error.unknown-error') + error.message
|
||||
break
|
||||
}
|
||||
this.toastError(errorMessage)
|
||||
this.toastError(this.$t('error.unknown-error') + error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||