Merge branch 'master' into Docu-Template-Overview-2021

This commit is contained in:
Alexander Friedland 2021-12-30 13:54:22 +01:00 committed by GitHub
commit f90c5a2532
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2915 additions and 526 deletions

View File

@ -47,7 +47,7 @@ jobs:
##########################################################################
- name: Admin | Build `test` image
run: |
docker build --target test -t "gradido/admin:test" admin/
docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
docker save "gradido/admin:test" > /tmp/admin.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
@ -294,6 +294,35 @@ jobs:
- name: Admin Interface | Lint
run: docker run --rm gradido/admin:test yarn run lint
##############################################################################
# JOB: LOCALES ADMIN ######################################################
##############################################################################
locales_admin:
name: Locales - Admin
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v2
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: admin | Locales
run: docker run --rm gradido/admin:test yarn run locales
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################
@ -480,7 +509,7 @@ jobs:
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb database
- name: backend Unit tests | test
run: cd database && yarn && cd ../backend && yarn && yarn test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_worklfow_test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
@ -491,7 +520,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 37
min_coverage: 40
token: ${{ github.token }}
##############################################################################

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
*.log
/node_modules/*
.vscode
messages.pot
nbproject
.metadata

View File

@ -2,6 +2,7 @@
"recommendations": [
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"hediet.vscode-drawio"
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}

View File

@ -13,7 +13,7 @@ ENV BUILD_VERSION="0.0.0.0"
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ENV BUILD_COMMIT="0000000"
## SET NODE_ENV
ENV NODE_ENV="production"
ARG NODE_ENV="production"
## App relevant Envs
ENV PORT="8080"

View File

@ -1,4 +1,15 @@
module.exports = {
presets: ['@babel/preset-env'],
plugins: ['transform-require-context'],
module.exports = function (api) {
api.cache(true)
const presets = ['@babel/preset-env']
const plugins = []
if (process.env.NODE_ENV === 'test') {
plugins.push('transform-require-context')
}
return {
presets,
plugins,
}
}

View File

@ -12,7 +12,8 @@
"dev": "yarn run serve",
"build": "vue-cli-service build",
"lint": "eslint --ext .js,.vue .",
"test": "jest --coverage"
"test": "jest --coverage",
"locales": "scripts/missing-keys.sh && scripts/sort.sh"
},
"dependencies": {
"@babel/core": "^7.15.8",

17
admin/scripts/missing-keys.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
ROOT_DIR=$(dirname "$0")/..
sorting="jq -f $ROOT_DIR/scripts/sort_filter.jq"
english="$sorting $ROOT_DIR/src/locales/en.json"
german="$sorting $ROOT_DIR/src/locales/de.json"
listPaths="jq -c 'path(..)|[.[]|tostring]|join(\".\")'"
diffString="<( $english | $listPaths ) <( $german | $listPaths )"
if eval "diff -q $diffString";
then
: # all good
else
eval "diff -y $diffString | grep '[|<>]'";
printf "\nEnglish and German translation keys do not match, see diff above.\n"
exit 1
fi

25
admin/scripts/sort.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
ROOT_DIR=$(dirname "$0")/..
tmp=$(mktemp)
exit_code=0
for locale_file in $ROOT_DIR/src/locales/*.json
do
jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp"
if [ "$*" == "--fix" ]
then
mv "$tmp" $locale_file
else
if diff -q "$tmp" $locale_file > /dev/null ;
then
: # all good
else
exit_code=$?
echo "$(basename -- $locale_file) is not sorted by keys"
fi
fi
done
exit $exit_code

View File

@ -0,0 +1,13 @@
def walk(f):
. as $in
| if type == "object" then
reduce keys_unsorted[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def keys_sort_by(f):
to_entries | sort_by(.key|f ) | from_entries;
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)

View File

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

View File

@ -0,0 +1,56 @@
<template>
<div class="component-confirm-register-mail">
<div class="shadow p-3 mb-5 bg-white rounded">
<div class="h5">
{{ $t('unregister_mail.text', { date: dateLastSend, mail: email }) }}
</div>
<!-- Using components -->
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">
<b-form-input readonly :value="email"></b-form-input>
<b-input-group-append>
<b-button variant="outline-success" class="test-button" @click="sendRegisterMail">
{{ $t('unregister_mail.button') }}
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
</template>
<script>
import { sendActivationEmail } from '../graphql/sendActivationEmail'
export default {
name: 'ConfirmRegisterMail',
props: {
email: {
type: String,
},
dateLastSend: {
type: String,
},
},
methods: {
sendRegisterMail() {
this.$apollo
.mutate({
mutation: sendActivationEmail,
variables: {
email: this.email,
},
})
.then(() => {
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
})
.catch((error) => {
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
})
},
},
}
</script>
<style>
.input-group-text {
background-color: rgb(255, 252, 205);
}
</style>

View File

@ -3,8 +3,8 @@
<hr />
<br />
<div class="text-center">
Gradido Akademie Adminkonsole
<div><small>Version: 0.0.1</small></div>
{{ $t('gradido_admin_footer') }}
<div><small>Version: 1.0.0</small></div>
</div>
</div>
</template>

View File

@ -21,6 +21,7 @@ const toastedErrorMock = jest.fn()
const toastedSuccessMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$moment: jest.fn(() => {
return {
format: jest.fn((m) => m),
@ -176,8 +177,8 @@ describe('CreationFormular', () => {
await wrapper.find('.test-submit').trigger('click')
})
it('sends ... to apollo', () => {
expect(toastedErrorMock).toBeCalled()
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Ouch!')
})
})

View File

@ -1,9 +1,10 @@
<template>
<div class="component-creation-formular">
{{ $t('creation_form.form') }}
<div class="shadow p-3 mb-5 bg-white rounded">
<b-form ref="creationForm">
<b-row class="m-4">
<label>Monat Auswählen</label>
<label>{{ $t('creation_form.select_month') }}</label>
<b-col class="text-left">
<b-form-radio
id="beforeLastMonth"
@ -49,7 +50,7 @@
</b-row>
<b-row class="m-4" v-show="createdIndex != null">
<label>Betrag Auswählen</label>
<label>{{ $t('creation_form.select_value') }}</label>
<div>
<b-input-group prepend="GDD" append=".00">
<b-form-input
@ -72,13 +73,13 @@
</div>
</b-row>
<b-row class="m-4">
<label>Text eintragen</label>
<label>{{ $t('creation_form.enter_text') }}</label>
<div>
<b-form-textarea
id="textarea-state"
v-model="text"
:state="text.length >= 10"
placeholder="Mindestens 10 Zeichen eingeben"
:placeholder="$t('creation_form.min_characters')"
rows="3"
></b-form-textarea>
</div>
@ -86,7 +87,7 @@
<b-row class="m-4">
<b-col class="text-center">
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
zurücksetzen
{{ $t('creation_form.reset') }}
</b-button>
</b-col>
<b-col class="text-center">
@ -99,7 +100,7 @@
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
>
Update Schöpfung ({{ type }},{{ pagetype }})
{{ $t('creation_form.update_creation') }}
</b-button>
<b-button
@ -110,7 +111,7 @@
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
>
Schöpfung einreichen ({{ type }})
{{ $t('creation_form.submit_creation') }}
</b-button>
</div>
</b-col>
@ -170,14 +171,17 @@ export default {
currentMonth: {
short: this.$moment().format('MMMM'),
long: this.$moment().format('YYYY-MM-DD'),
year: this.$moment().format('YYYY'),
},
lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'),
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
year: this.$moment().subtract(1, 'month').format('YYYY'),
},
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
year: this.$moment().subtract(2, 'month').format('YYYY'),
},
submitObj: null,
isdisabled: true,
@ -189,6 +193,7 @@ export default {
// Auswählen eines Zeitraumes
updateRadioSelected(name, index, openCreation) {
this.createdIndex = index
this.text = this.$t('creation_form.creation_for') + ' ' + name.short + ' ' + name.year
// Wenn Mehrfachschöpfung
if (this.type === 'massCreation') {
// An Creation.vue emitten und radioSelectedMass aktualisieren
@ -204,8 +209,6 @@ export default {
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
const i = Object.keys(this.itemsMassCreation).length
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
// eslint-disable-next-line no-console
console.log('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
this.submitObj = [
{
item: this.itemsMassCreation,
@ -216,8 +219,6 @@ export default {
moderator: this.$store.state.moderator.id,
},
]
// eslint-disable-next-line no-console
console.log('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
// $store - offene Schöpfungen hochzählen
this.$store.commit('openCreationsPlus', i)
@ -241,7 +242,10 @@ export default {
.then((result) => {
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
this.$toasted.success(
`Offene Schöpfung (${this.value} GDD) für ${this.item.email} wurde gespeichert und liegen zur Bestätigung bereit`,
this.$t('creation_form.toasted', {
value: this.value,
email: this.item.email,
}),
)
this.$store.commit('openCreationsPlus', 1)
this.submitObj = null

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<div class="shadow p-3 mb-5 bg-white rounded">
<b-form ref="updateCreationForm">
<b-row class="m-4">
<label>Monat Auswählen</label>
<label>{{ $t('creation_form.select_month') }}</label>
<b-col class="text-left">
<b-form-radio
id="beforeLastMonth"
@ -64,7 +64,7 @@
</b-row>
<b-row class="m-4">
<label>Betrag Auswählen</label>
<label>{{ $t('creation_form.select_value') }}</label>
<div>
<b-input-group prepend="GDD" append=".00">
<b-form-input
@ -87,7 +87,7 @@
</div>
</b-row>
<b-row class="m-4">
<label>Text eintragen</label>
<label>{{ $t('creation_form.enter_text') }}</label>
<div>
<b-form-textarea
id="textarea-state"
@ -101,7 +101,7 @@
<b-row class="m-4">
<b-col class="text-center">
<b-button type="reset" variant="danger" @click="$refs.updateCreationForm.reset()">
zurücksetzen
{{ $t('creation_form.reset') }}
</b-button>
</b-col>
<b-col class="text-center">
@ -113,7 +113,7 @@
@click="submitCreation"
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
>
Update Schöpfung ({{ type }},{{ pagetype }})
{{ $t('creation_form.update_creation') }}
</b-button>
</div>
</b-col>
@ -127,15 +127,6 @@ import { updatePendingCreation } from '../graphql/updatePendingCreation'
export default {
name: 'EditCreationFormular',
props: {
type: {
type: String,
required: false,
},
pagetype: {
type: String,
required: false,
default: '',
},
item: {
type: Object,
required: false,
@ -143,13 +134,6 @@ export default {
return {}
},
},
items: {
type: Array,
required: false,
default() {
return []
},
},
row: {
type: Object,
required: false,
@ -227,7 +211,10 @@ export default {
row: this.row,
})
this.$toasted.success(
`Offene schöpfung (${this.value} GDD) für ${this.item.email} wurde geändert, liegt zur Bestätigung bereit`,
this.$t('creation_form.toasted_update', {
value: this.value,
email: this.item.email,
}),
)
this.submitObj = null
this.createdIndex = null
@ -247,7 +234,7 @@ export default {
},
},
created() {
if (this.pagetype === 'PageCreationConfirm' && this.creationUserData.date) {
if (this.creationUserData.date) {
switch (this.$moment(this.creationUserData.date).format('MMMM')) {
case this.currentMonth.short:
this.createdIndex = 2

View File

@ -7,6 +7,7 @@ const storeDispatchMock = jest.fn()
const routerPushMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
openCreations: 1,

View File

@ -1,6 +1,6 @@
<template>
<div class="component-nabvar">
<b-navbar toggleable="sm" type="dark" variant="success">
<b-navbar toggleable="md" type="dark" variant="success" class="p-3">
<b-navbar-brand to="/">
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
</b-navbar-brand>
@ -9,19 +9,18 @@
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/">Übersicht |</b-nav-item>
<b-nav-item to="/user">Usersuche |</b-nav-item>
<b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
<b-nav-item to="/">{{ $t('navbar.overview') }}</b-nav-item>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item>
<b-nav-item
v-show="$store.state.openCreations > 0"
class="h5 bg-danger"
class="bg-color-creation p-1"
to="/creation-confirm"
>
| {{ $store.state.openCreations }} offene Schöpfungen
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
</b-nav-item>
<b-nav-item @click="wallet">Wallet</b-nav-item>
<b-nav-item @click="logout">Logout</b-nav-item>
<!-- <b-nav-item v-show="open < 1" to="/creation-confirm">| keine offene Schöpfungen</b-nav-item> -->
<b-nav-item @click="wallet">{{ $t('navbar.wallet') }}</b-nav-item>
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
@ -49,4 +48,7 @@ export default {
height: 2rem;
padding-left: 10px;
}
.bg-color-creation {
background-color: #cf1010dc;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<b-card class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
<b-row class="mb-2">
<b-col></b-col>
</b-row>
<slot :name="slotName" />
<b-button size="sm" @click="$emit('row-toogle-details', row, index)">
<b-icon
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help"
></b-icon>
{{ $t('hide_details') }} {{ row.item.firstName }} {{ row.item.lastName }}
</b-button>
</b-card>
</template>
<script>
export default {
name: 'RowDetails',
props: {
row: { required: true, type: Object },
slotName: { requried: true, type: String },
type: { requried: true, type: String },
index: { requried: true, type: Number },
},
}
</script>

View File

@ -13,8 +13,12 @@ describe('UserTable', () => {
creation: [],
}
const mocks = {
$t: jest.fn((t) => t),
}
const Wrapper = () => {
return mount(UserTable, { localVue, propsData })
return mount(UserTable, { localVue, propsData, mocks })
}
describe('mount', () => {

View File

@ -36,63 +36,86 @@
hover
stacked="md"
>
<template #cell(creation)="data">
<div v-html="data.value"></div>
</template>
<template #cell(edit_creation)="row">
<b-button
variant="info"
size="md"
@click="editCreationUserTable(row, row.item)"
class="mr-2"
>
<b-icon v-if="row.detailsShowing" icon="x" aria-label="Help"></b-icon>
<b-icon v-else icon="pencil-square" aria-label="Help"></b-icon>
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
</template>
<template #cell(show_details)="row">
<b-button variant="info" size="md" @click="row.toggleDetails" class="mr-2">
<b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
<b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
<b-icon :icon="row.detailsShowing ? 'eye-slash-fill' : 'eye-fill'"></b-icon>
</b-button>
</template>
<template #cell(confirm_mail)="row">
<b-button
:variant="row.item.emailChecked ? 'success' : 'danger'"
size="md"
@click="rowToogleDetails(row, 1)"
class="mr-2"
>
<b-icon
:icon="row.item.emailChecked ? 'envelope-open' : 'envelope'"
aria-label="Help"
></b-icon>
</b-button>
</template>
<template #cell(transactions_list)="row">
<b-button variant="warning" size="md" @click="rowToogleDetails(row, 2)" class="mr-2">
<b-icon icon="list"></b-icon>
</b-button>
</template>
<template #row-details="row">
<b-card class="shadow-lg p-3 mb-5 bg-white rounded">
<b-row class="mb-2">
<b-col></b-col>
</b-row>
{{ type }}
<creation-formular
v-if="type === 'PageUserSearch'"
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<edit-creation-formular
v-else
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<b-button size="sm" @click="row.toggleDetails">
<b-icon
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help"
></b-icon>
Details verbergen von {{ row.item.firstName }} {{ row.item.lastName }}
</b-button>
</b-card>
<row-details
:row="row"
:type="type"
:slotName="slotName"
:index="slotIndex"
@row-toogle-details="rowToogleDetails"
>
<template #show-creation>
<div>
<creation-formular
v-if="type === 'PageUserSearch'"
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
<edit-creation-formular
v-else
type="singleCreation"
:pagetype="type"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/>
</div>
</template>
<template #show-register-mail>
<confirm-register-mail-formular
:email="row.item.email"
:dateLastSend="$moment().subtract(1, 'month').format('dddd, DD.MMMM.YYYY HH:mm'),"
/>
</template>
<template #show-transaction-list>
<creation-transaction-list-formular :userId="row.item.userId" />
</template>
</row-details>
</template>
<template #cell(bookmark)="row">
<b-button
variant="warning"
@ -132,8 +155,14 @@
<script>
import CreationFormular from '../components/CreationFormular.vue'
import EditCreationFormular from '../components/EditCreationFormular.vue'
import ConfirmRegisterMailFormular from '../components/ConfirmRegisterMailFormular.vue'
import CreationTransactionListFormular from '../components/CreationTransactionListFormular.vue'
import RowDetails from '../components/RowDetails.vue'
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
const slotNames = ['show-creation', 'show-register-mail', 'show-transaction-list']
export default {
name: 'UserTable',
props: {
@ -162,9 +191,15 @@ export default {
components: {
CreationFormular,
EditCreationFormular,
ConfirmRegisterMailFormular,
CreationTransactionListFormular,
RowDetails,
},
data() {
return {
showCreationFormular: null,
showConfirmRegisterMailFormular: null,
showCreationTransactionListFormular: null,
creationUserData: {},
overlay: false,
overlayBookmarkType: '',
@ -178,30 +213,53 @@ export default {
button_cancel: 'Cancel',
},
],
slotIndex: 0,
openRow: null,
}
},
methods: {
rowToogleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
}
} else {
row.toggleDetails()
this.slotIndex = index
this.openRow = row
if (this.type === 'PageCreationConfirm') {
this.creationUserData = row.item
}
}
},
overlayShow(bookmarkType, item) {
this.overlay = true
this.overlayBookmarkType = bookmarkType
this.overlayItem = item
if (bookmarkType === 'remove') {
this.overlayText.header = 'Achtung! Schöpfung löschen!'
this.overlayText.text1 =
'Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.'
this.overlayText.text2 = 'Willst du die vorgespeicherte Schöpfung wirklich löschen? '
this.overlayText.button_ok = 'Ja, Schöpfung löschen!'
this.overlayText.button_cancel = 'Nein, nicht löschen.'
this.overlayText.header = this.$t('overlay.remove.title')
this.overlayText.text1 = this.$t('overlay.remove.text')
this.overlayText.text2 = this.$t('overlay.remove.question')
this.overlayText.button_ok = this.$t('overlay.remove.yes')
this.overlayText.button_cancel = this.$t('overlay.remove.no')
}
if (bookmarkType === 'confirm') {
this.overlayText.header = 'Schöpfung bestätigen!'
this.overlayText.text1 =
'Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.'
this.overlayText.text2 =
'Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?'
this.overlayText.button_ok = 'Ja, Schöpfung speichern und bestätigen!'
this.overlayText.button_cancel = 'Nein, nicht speichern.'
this.overlayText.header = this.$t('overlay.confirm.title')
this.overlayText.text1 = this.$t('overlay.confirm.text')
this.overlayText.text2 = this.$t('overlay.confirm.question')
this.overlayText.button_ok = this.$t('overlay.confirm.yes')
this.overlayText.button_cancel = this.$t('overlay.confirm.no')
}
},
overlayOK(bookmarkType, item) {
@ -237,20 +295,12 @@ export default {
},
})
.then(() => {
this.$emit('remove-confirm-result', item, 'remove')
this.$emit('remove-confirm-result', item, 'confirmed')
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
editCreationUserTable(row, rowItem) {
if (!row.detailsShowing) {
this.creationUserData = rowItem
} else {
this.creationUserData = {}
}
row.toggleDetails()
},
updateCreationData(data) {
this.creationUserData.amount = data.amount
this.creationUserData.date = data.date
@ -263,6 +313,11 @@ export default {
rowItem.creation = newCreation
},
},
computed: {
slotName() {
return slotNames[this.slotIndex]
},
},
}
</script>
<style>

View File

@ -3,7 +3,7 @@ import gql from 'graphql-tag'
export const createPendingCreation = gql`
mutation (
$email: String!
$amount: Int!
$amount: Float!
$memo: String!
$creationDate: String!
$moderator: Int!

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ export const updatePendingCreation = gql`
mutation (
$id: Int!
$email: String!
$amount: Int!
$amount: Float!
$memo: String!
$creationDate: String!
$moderator: Int!

View File

@ -4,7 +4,7 @@ import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const loadLocaleMessages = () => {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const locales = require.context('./locales/', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach((key) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)

View File

@ -1 +1,65 @@
{}
{
"bookmark": "bookmark",
"confirmed": "bestätigt",
"creation_form": {
"creation_for": "Schöpfung für ",
"enter_text": "Text eintragen",
"form": "Schöpfungsformular",
"min_characters": "Mindestens 10 Zeichen eingeben",
"reset": "Zurücksetzen",
"select_month": "Monat auswählen",
"select_value": "Betrag auswählen",
"submit_creation": "Schöpfung einreichen",
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
"update_creation": "Schöpfung aktualisieren"
},
"details": "Details",
"e_mail": "E-Mail",
"firstname": "Vorname",
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
"hide_details": "Details verbergen von",
"lastname": "Nachname",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"navbar": {
"logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
"user_search": "Nutzersuche",
"wallet": "Wallet"
},
"not_open_creations": "Keine offenen Schöpfungen",
"open_creation": "Offene Schöpfung",
"open_creations": "Offene Schöpfungen",
"overlay": {
"confirm": {
"no": "Nein, nicht speichern.",
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
"title": "Schöpfung bestätigen!",
"yes": "Ja, Schöpfung bestätigen und speichern!"
},
"remove": {
"no": "Nein, nicht löschen.",
"question": "Willst du die vorgespeicherte Schöpfung wirklich löschen?",
"text": "Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.",
"title": "Achtung! Schöpfung löschen!",
"yes": "Ja, Schöpfung löschen!"
}
},
"remove": "Entfernen",
"transaction": "Transaktion",
"transactionlist": {
"title": "Alle geschöpften Transaktionen für den Nutzer"
},
"unregistered_emails": "Unregistrierte E-Mails",
"unregister_mail": {
"button": "Registrierungs-Email bestätigen, jetzt senden",
"error": "Fehler beim Senden des Bestätigungs-Links an den Benutzer: {message}",
"info": "Email bestätigen, wiederholt senden an:",
"success": "Erfolgreiches Senden des Bestätigungs-Links an die E-Mail des Nutzers! ({email})",
"text": " Die letzte Email wurde am <b>{date} Uhr</b> an das Mitglied ({mail}) gesendet."
},
"user_search": "Nutzer-Suche"
}

View File

@ -1 +1,65 @@
{}
{
"bookmark": "Remember",
"confirmed": "confirmed",
"creation_form": {
"creation_for": "Creation for ",
"enter_text": "Enter text",
"form": "Creation form",
"min_characters": "Enter at least 10 characters",
"reset": "Reset",
"select_month": "Select month",
"select_value": "Select amount",
"submit_creation": "Submit creation",
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
"update_creation": "Creation update"
},
"details": "Details",
"e_mail": "E-Mail",
"firstname": "Firstname",
"gradido_admin_footer": "Gradido Academy Admin Console",
"hide_details": "Hide details from",
"lastname": "Lastname",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"navbar": {
"logout": "Logout",
"multi_creation": "Multiple creation",
"open_creation": "Open creations",
"overview": "Overview",
"user_search": "User search",
"wallet": "Wallet"
},
"not_open_creations": "No open creations",
"open_creation": "Open creation",
"open_creations": "Open creations",
"overlay": {
"confirm": {
"no": "No, do not save.",
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
"title": "Confirm creation!",
"yes": "Yes, confirm and save creation!"
},
"remove": {
"no": "No, do not delete.",
"question": "Do you really want to delete the pre-stored creation?",
"text": "After deletion, there is no possibility to restore this data record. However, the entire process is saved in the log file as an overview.",
"title": "Attention! Delete creation!",
"yes": "Yes, delete creation!"
}
},
"remove": "Remove",
"transaction": "Transaction",
"transactionlist": {
"title": "All creation-transactions for the user"
},
"unregistered_emails": "Unregistered e-mails",
"unregister_mail": {
"button": "Confirm registration email, send now",
"error": "Error sending the confirmation link to the user: {message}",
"info": "Confirm email, send repeatedly to:",
"success": "Successfully send the confirmation link to the user's email! ({email})",
"text": " The last email was sent to the member ({mail}) on <b>{date} clock</b>."
},
"user_search": "User search"
}

View File

@ -0,0 +1,16 @@
const locales = [
{
name: 'English',
code: 'en',
iso: 'en-US',
enabled: true,
},
{
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true,
},
]
export default locales

View File

@ -28,6 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
const toastErrorMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},

View File

@ -31,7 +31,7 @@
@update-item="updateItem"
/>
<div v-if="itemsMassCreation.length === 0">
Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
{{ $t('multiple_creation_text') }}
</div>
<creation-formular
v-else
@ -40,7 +40,6 @@
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmark"
/>
{{ itemsMassCreation }}
</b-col>
</b-row>
</div>
@ -60,18 +59,18 @@ export default {
return {
showArrays: false,
Searchfields: [
{ key: 'bookmark', label: 'merken' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
{ key: 'email', label: 'Email' },
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{ key: 'creation', label: this.$t('open_creations') },
{ key: 'email', label: this.$t('e_mail') },
],
fields: [
{ key: 'email', label: 'Email' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
{ key: 'bookmark', label: 'löschen' },
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{ key: 'creation', label: this.$t('open_creations') },
{ key: 'bookmark', label: this.$t('remove') },
],
itemsList: [],
itemsMassCreation: [],

View File

@ -37,6 +37,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
const apolloMutateMock = jest.fn().mockResolvedValue({})
const mocks = {
$t: jest.fn((t) => t),
$store: {
commit: storeCommitMock,
},
@ -81,7 +82,7 @@ describe('CreationConfirm', () => {
})
})
describe('confirm creation delete with success', () => {
describe('delete creation delete with success', () => {
beforeEach(async () => {
apolloQueryMock.mockResolvedValue({
data: {
@ -130,7 +131,7 @@ describe('CreationConfirm', () => {
})
})
describe('confirm creation delete with error', () => {
describe('delete creation delete with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper
@ -143,6 +144,67 @@ describe('CreationConfirm', () => {
})
})
describe('confirm creation delete with success', () => {
beforeEach(async () => {
apolloQueryMock.mockResolvedValue({
data: {
getPendingCreations: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 0,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergatert',
date: new Date(),
moderator: 0,
},
],
},
})
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirmed')
})
it('calls the deletePendingCreation mutation', () => {
expect(apolloMutateMock).not.toBeCalledWith({
mutation: deletePendingCreation,
variables: { id: 1 },
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
})
})
describe('delete creation delete with error', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'UserTable' })
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirm')
})
it('toasts an error message', () => {
expect(toastedErrorMock).toBeCalledWith('Case confirm is not supported')
})
})
describe('server response is error', () => {
beforeEach(() => {
jest.clearAllMocks()

View File

@ -51,25 +51,34 @@ export default {
},
methods: {
removeConfirmResult(e, event) {
if (event === 'remove') {
let index = 0
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
this.$apollo
.mutate({
mutation: deletePendingCreation,
variables: {
id: findArr.id,
},
})
.then((result) => {
index = this.confirmResult.indexOf(findArr)
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
})
.catch((error) => {
this.$toasted.error(error.message)
})
let index = 0
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
switch (event) {
case 'remove':
this.$apollo
.mutate({
mutation: deletePendingCreation,
variables: {
id: findArr.id,
},
})
.then((result) => {
index = this.confirmResult.indexOf(findArr)
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
})
.catch((error) => {
this.$toasted.error(error.message)
})
break
case 'confirmed':
this.confirmResult.splice(index, 1)
this.$store.commit('openCreationsMinus', 1)
this.$toasted.success('Pending Creation has been deleted')
break
default:
this.$toasted.error('Case ' + event + ' is not supported')
}
},
getPendingCreations() {
@ -80,7 +89,7 @@ export default {
})
.then((result) => {
this.$store.commit('resetOpenCreations')
this.confirmResult = result.data.getPendingCreations.reverse()
this.confirmResult = result.data.getPendingCreations
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
})
.catch((error) => {

View File

@ -22,6 +22,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},

View File

@ -3,7 +3,7 @@
<b-card
v-show="$store.state.openCreations > 0"
border-variant="primary"
header="offene Schöpfungen"
:header="$t('open_creations')"
header-bg-variant="danger"
header-text-variant="white"
align="center"
@ -17,7 +17,7 @@
<b-card
v-show="$store.state.openCreations < 1"
border-variant="success"
header="keine offene Schöpfungen"
:header="$t('not_open_creations')"
header-bg-variant="success"
header-text-variant="white"
align="center"
@ -28,28 +28,6 @@
</b-link>
</b-card-text>
</b-card>
<br />
<hr />
<br />
<b-list-group>
<b-list-group-item class="bg-secondary text-light" href="user">
zur Usersuche
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
Mitglieder
<b-badge class="bg-success" pill>2400</b-badge>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
aktive Mitglieder
<b-badge class="bg-primary" pill>2201</b-badge>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
nicht bestätigte Mitglieder
<b-badge class="bg-warning text-dark" pill>120</b-badge>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>

View File

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

View File

@ -1,19 +1,27 @@
<template>
<div class="user-search">
<label>Usersuche</label>
<div style="text-align: right">
<b-button block variant="danger" @click="unconfirmedRegisterMails">
<b-icon icon="envelope" variant="light"></b-icon>
{{ $t('unregistered_emails') }}
</b-button>
</div>
<label>{{ $t('user_search') }}</label>
<b-input
type="text"
v-model="criteria"
class="shadow p-3 mb-5 bg-white rounded"
placeholder="User suche"
class="shadow p-3 mb-3 bg-white rounded"
:placeholder="$t('user_search')"
@input="getUsers"
></b-input>
<user-table
type="PageUserSearch"
:itemsUser="searchResult"
:fieldsTable="fields"
:criteria="criteria"
/>
<div></div>
</div>
</template>
<script>
@ -29,25 +37,59 @@ export default {
return {
showArrays: false,
fields: [
{ key: 'email', label: 'Email' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: 'Creation',
label: this.$t('open_creation'),
formatter: (value, key, item) => {
return String(value)
return (
`
<div>` +
this.$moment().subtract(2, 'month').format('MMMM') +
` - ` +
String(value[0]) +
` GDD</div>
<div>` +
this.$moment().subtract(1, 'month').format('MMMM') +
` - ` +
String(value[1]) +
` GDD</div>
<div>` +
this.$moment().format('MMMM') +
` - ` +
String(value[2]) +
` GDD</div>
`
)
},
},
{ key: 'show_details', label: 'Details' },
{ key: 'show_details', label: this.$t('details') },
{ key: 'confirm_mail', label: this.$t('confirmed') },
{ key: 'transactions_list', label: this.$t('transaction') },
],
searchResult: [],
massCreation: [],
criteria: '',
currentMonth: {
short: this.$moment().format('MMMM'),
},
lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'),
},
beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'),
},
}
},
methods: {
unconfirmedRegisterMails() {
this.searchResult = this.searchResult.filter((user) => {
return user.emailChecked
})
},
getUsers() {
this.$apollo
.query({
@ -57,12 +99,7 @@ export default {
},
})
.then((result) => {
this.searchResult = result.data.searchUsers.map((user) => {
return {
...user,
// showDetails: true,
}
})
this.searchResult = result.data.searchUsers
})
.catch((error) => {
this.$toasted.error(error.message)

View File

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

View File

@ -5,6 +5,11 @@ module.exports = {
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
moduleNameMapper: {
'@entity/(.*)': '<rootDir>/../database/entity/$1',
'@entity/(.*)': '<rootDir>/../database/build/entity/$1',
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
'@dbTools/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',
},
}

View File

@ -13,7 +13,8 @@
"start": "node build/index.js",
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
"lint": "eslint . --ext .js,.ts",
"test": "jest --runInBand --coverage "
"CI_worklfow_test": "jest --runInBand --coverage ",
"test": "NODE_ENV=development jest --runInBand --coverage "
},
"dependencies": {
"@types/jest": "^27.0.2",
@ -59,6 +60,7 @@
"typescript": "^4.3.4"
},
"_moduleAliases": {
"@entity": "../database/build/entity"
"@entity": "../database/build/entity",
"@dbTools": "../database/build/src"
}
}

View File

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

View File

@ -1,11 +1,11 @@
import { ArgsType, Field, Int } from 'type-graphql'
import { ArgsType, Field, Float, Int } from 'type-graphql'
@ArgsType()
export default class CreatePendingCreationArgs {
@Field(() => String)
email: string
@Field(() => Int)
@Field(() => Float)
amount: number
@Field(() => String)

View File

@ -11,9 +11,6 @@ export default class CreateUserArgs {
@Field(() => String)
lastName: string
@Field(() => String)
password: string
@Field(() => String)
language?: string // Will default to DEFAULT_LANGUAGE

View File

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

View File

@ -1,4 +1,4 @@
import { ArgsType, Field, Int } from 'type-graphql'
import { ArgsType, Field, Float, Int } from 'type-graphql'
@ArgsType()
export default class CreatePendingCreationArgs {
@ -8,7 +8,7 @@ export default class CreatePendingCreationArgs {
@Field(() => String)
email: string
@Field(() => Int)
@Field(() => Float)
amount: number
@Field(() => String)

View File

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

View File

@ -17,6 +17,7 @@ import { UserTransaction } from '@entity/UserTransaction'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { calculateDecay } from '../../util/decay'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
@Resolver()
export class AdminResolver {
@ -28,10 +29,12 @@ export class AdminResolver {
const adminUsers = await Promise.all(
users.map(async (user) => {
const adminUser = new UserAdmin()
adminUser.userId = user.id
adminUser.firstName = user.firstName
adminUser.lastName = user.lastName
adminUser.email = user.email
adminUser.creation = await getUserCreations(user.id)
adminUser.emailChecked = await hasActivatedEmail(user.email)
return adminUser
}),
)
@ -133,7 +136,7 @@ export class AdminResolver {
return newPendingCreation
}),
)
return pendingCreationsPromise
return pendingCreationsPromise.reverse()
}
@Mutation(() => Boolean)
@ -212,95 +215,80 @@ export class AdminResolver {
async function getUserCreations(id: number): Promise<number[]> {
const dateNextMonth = moment().add(1, 'month').format('YYYY-MM') + '-01'
const dateMonth = moment().format('YYYY-MM') + '-01'
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM') + '-01'
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM') + '-01'
const beforeLastMonthNumber = moment().subtract(2, 'month').format('M')
const lastMonthNumber = moment().subtract(1, 'month').format('M')
const currentMonthNumber = moment().format('M')
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
const createdAmountBeforeLastMonth = await transactionCreationRepository
const createdAmountsQuery = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.select('MONTH(transaction_creations.target_date)', 'target_month')
.addSelect('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
date: dateBeforeLastMonth,
enddate: dateLastMonth,
endDate: dateNextMonth,
}),
})
.getRawOne()
const createdAmountLastMonth = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateLastMonth,
enddate: dateMonth,
}),
})
.getRawOne()
const createdAmountMonth = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateMonth,
enddate: dateNextMonth,
}),
})
.getRawOne()
.groupBy('target_month')
.orderBy('target_month', 'ASC')
.getRawMany()
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
const pendingAmountMounth = await loginPendingTasksAdminRepository
const pendingAmountsQuery = await loginPendingTasksAdminRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.select('MONTH(login_pending_tasks_admin.date)', 'target_month')
.addSelect('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateMonth,
enddate: dateNextMonth,
}),
})
.getRawOne()
const pendingAmountLastMounth = await loginPendingTasksAdminRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateLastMonth,
enddate: dateMonth,
}),
})
.getRawOne()
const pendingAmountBeforeLastMounth = await loginPendingTasksAdminRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
date: dateBeforeLastMonth,
enddate: dateLastMonth,
endDate: dateNextMonth,
}),
})
.getRawOne()
.groupBy('target_month')
.orderBy('target_month', 'ASC')
.getRawMany()
const map = new Map()
if (Array.isArray(createdAmountsQuery) && createdAmountsQuery.length > 0) {
createdAmountsQuery.forEach((createdAmount) => {
if (!map.has(createdAmount.target_month)) {
map.set(createdAmount.target_month, createdAmount.sum)
} else {
const store = map.get(createdAmount.target_month)
map.set(createdAmount.target_month, Number(store) + Number(createdAmount.sum))
}
})
}
if (Array.isArray(pendingAmountsQuery) && pendingAmountsQuery.length > 0) {
pendingAmountsQuery.forEach((pendingAmount) => {
if (!map.has(pendingAmount.target_month)) {
map.set(pendingAmount.target_month, pendingAmount.sum)
} else {
const store = map.get(pendingAmount.target_month)
map.set(pendingAmount.target_month, Number(store) + Number(pendingAmount.sum))
}
})
}
const usedCreationBeforeLastMonth = map.get(Number(beforeLastMonthNumber))
? Number(map.get(Number(beforeLastMonthNumber))) / 10000
: 0
const usedCreationLastMonth = map.get(Number(lastMonthNumber))
? Number(map.get(Number(lastMonthNumber))) / 10000
: 0
const usedCreationCurrentMonth = map.get(Number(currentMonthNumber))
? Number(map.get(Number(currentMonthNumber))) / 10000
: 0
// COUNT amount from 2 tables
const usedCreationBeforeLastMonth =
(Number(createdAmountBeforeLastMonth.sum) + Number(pendingAmountBeforeLastMounth.sum)) / 10000
const usedCreationLastMonth =
(Number(createdAmountLastMonth.sum) + Number(pendingAmountLastMounth.sum)) / 10000
const usedCreationMonth =
(Number(createdAmountMonth.sum) + Number(pendingAmountMounth.sum)) / 10000
return [
1000 - usedCreationBeforeLastMonth,
1000 - usedCreationLastMonth,
1000 - usedCreationMonth,
1000 - usedCreationCurrentMonth,
]
}
@ -330,3 +318,8 @@ function isCreationValid(creations: number[], amount: number, creationDate: Date
}
return true
}
async function hasActivatedEmail(email: string): Promise<boolean> {
const repository = getCustomRepository(LoginUserRepository)
const user = await repository.findByEmail(email)
return user ? user.emailChecked : false
}

View File

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

View File

@ -0,0 +1,232 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import { GraphQLError } from 'graphql'
import createServer from '../../server/createServer'
import { resetDB, initialize } from '@dbTools/helpers'
import { getRepository } from 'typeorm'
import { LoginUser } from '@entity/LoginUser'
import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import CONFIG from '../../config'
import { sendEMail } from '../../util/sendEMail'
jest.mock('../../util/sendEMail', () => {
return {
__esModule: true,
sendEMail: jest.fn(),
}
})
let mutate: any
let con: any
beforeAll(async () => {
const server = await createServer({})
con = server.con
mutate = createTestClient(server.apollo).mutate
await initialize()
await resetDB()
})
describe('UserResolver', () => {
describe('createUser', () => {
const variables = {
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
language: 'de',
publisherId: 1234,
}
const mutation = gql`
mutation (
$email: String!
$firstName: String!
$lastName: String!
$language: String!
$publisherId: Int
) {
createUser(
email: $email
firstName: $firstName
lastName: $lastName
language: $language
publisherId: $publisherId
)
}
`
let result: any
let emailOptIn: string
let newUser: User
beforeAll(async () => {
result = await mutate({ mutation, variables })
})
afterAll(async () => {
await resetDB()
})
it('returns success', () => {
expect(result).toEqual(expect.objectContaining({ data: { createUser: 'success' } }))
})
describe('valid input data', () => {
let loginUser: LoginUser[]
let user: User[]
let loginUserBackup: LoginUserBackup[]
let loginEmailOptIn: LoginEmailOptIn[]
beforeAll(async () => {
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
user = await getRepository(User).createQueryBuilder('state_user').getMany()
loginUserBackup = await getRepository(LoginUserBackup)
.createQueryBuilder('login_user_backup')
.getMany()
loginEmailOptIn = await getRepository(LoginEmailOptIn)
.createQueryBuilder('login_email_optin')
.getMany()
newUser = user[0]
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
})
describe('filling all tables', () => {
it('saves the user in login_user table', () => {
expect(loginUser).toEqual([
{
id: expect.any(Number),
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
username: '',
description: '',
password: '0',
pubKey: null,
privKey: null,
emailHash: expect.any(Buffer),
createdAt: expect.any(Date),
emailChecked: false,
passphraseShown: false,
language: 'de',
disabled: false,
groupId: 1,
publisherId: 1234,
},
])
})
it('saves the user in state_user table', () => {
expect(user).toEqual([
{
id: expect.any(Number),
indexId: 0,
groupId: 0,
pubkey: expect.any(Buffer),
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
username: '',
disabled: false,
},
])
})
it('saves the user in login_user_backup table', () => {
expect(loginUserBackup).toEqual([
{
id: expect.any(Number),
passphrase: expect.any(String),
userId: loginUser[0].id,
mnemonicType: 2,
},
])
})
it('creates an email optin', () => {
expect(loginEmailOptIn).toEqual([
{
id: expect.any(Number),
userId: loginUser[0].id,
verificationCode: expect.any(String),
emailOptInTypeId: 1,
createdAt: expect.any(Date),
resendCount: 0,
updatedAt: expect.any(Date),
},
])
})
})
})
describe('account activation email', () => {
it('sends an account activation email', () => {
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(/\$1/g, emailOptIn)
expect(sendEMail).toBeCalledWith({
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
to: `${newUser.firstName} ${newUser.lastName} <${newUser.email}>`,
subject: 'Gradido: E-Mail Überprüfung',
text:
expect.stringContaining(`Hallo ${newUser.firstName} ${newUser.lastName},`) &&
expect.stringContaining(activationLink),
})
})
})
describe('email already exists', () => {
it('throws an error', async () => {
await expect(mutate({ mutation, variables })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User already exists.')],
}),
)
})
})
describe('unknown language', () => {
it('sets "de" as default language', async () => {
await mutate({
mutation,
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
})
await expect(
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
email: 'bibi@bloxberg.de',
language: 'de',
}),
]),
)
})
})
describe('no publisher id', () => {
it('sets publisher id to null', async () => {
await mutate({
mutation,
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
})
await expect(
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
email: 'raeuber@hotzenplotz.de',
publisherId: null,
}),
]),
)
})
})
})
})
afterAll(async () => {
await resetDB(true)
await con.close()
})

View File

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

View File

@ -29,7 +29,7 @@ import { elopageWebhook } from '../webhook/elopage'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0005-admin_tables'
const DB_VERSION = '0006-login_users_collation'
const createServer = async (context: any = serverContext): Promise<any> => {
// open mysql connection

View File

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

View File

@ -146,7 +146,6 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
firstName,
lastName,
publisherId: loginElopgaeBuy.publisherId,
password: '123', // TODO remove
})
} catch (error) {
// eslint-disable-next-line no-console

View File

@ -47,7 +47,8 @@
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"@entity/*": ["../database/entity/*"]
"@entity/*": ["../database/entity/*"],
"@dbTools/*": ["../database/src/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */

View File

@ -4,6 +4,5 @@ DB_USER=root
DB_PASSWORD=
DB_DATABASE=gradido_community
MIGRATIONS_TABLE=migrations
MIGRATIONS_DIRECTORY=./migrations/
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}

View File

@ -1,2 +0,0 @@
// For production you need to put your env file in here.
// Please copy the dist file from the root folder in here and rename it to .env

View File

@ -2,7 +2,7 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'ty
import { Balance } from '../Balance'
// Moriz: I do not like the idea of having two user tables
@Entity('state_users')
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@ -16,16 +16,28 @@ export class User extends BaseEntity {
@Column({ type: 'binary', length: 32, name: 'public_key' })
pubkey: Buffer
@Column({ length: 255, nullable: true, default: null })
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ length: 255, nullable: true, default: null })
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string
@Column()

View File

@ -2,7 +2,7 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 't
import { UserSetting } from './UserSetting'
// Moriz: I do not like the idea of having two user tables
@Entity('state_users')
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@ -16,16 +16,28 @@ export class User extends BaseEntity {
@Column({ type: 'binary', length: 32, name: 'public_key' })
pubkey: Buffer
@Column({ length: 255, nullable: true, default: null })
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ length: 255, nullable: true, default: null })
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string
@Column()

View File

@ -0,0 +1,60 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { LoginUserBackup } from '../LoginUserBackup'
// Moriz: I do not like the idea of having two user tables
@Entity('login_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class LoginUser extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 191, unique: true, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({ name: 'first_name', length: 150, collation: 'utf8mb4_unicode_ci' })
firstName: string
@Column({ name: 'last_name', length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
lastName: string
@Column({ length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
username: string
@Column({ default: '', collation: 'utf8mb4_unicode_ci' })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'pubkey', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date
@Column({ name: 'email_checked', default: 0 })
emailChecked: boolean
@Column({ name: 'passphrase_shown', default: 0 })
passphraseShown: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci' })
language: string
@Column({ default: 0 })
disabled: boolean
@Column({ name: 'group_id', default: 0, unsigned: true })
groupId: number
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToOne(() => LoginUserBackup, (loginUserBackup) => loginUserBackup.loginUser)
loginUserBackup: LoginUserBackup
}

View File

@ -1 +1 @@
export { LoginUser } from './0003-login_server_tables/LoginUser'
export { LoginUser } from './0006-login_users_collation/LoginUser'

View File

@ -1,15 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* FIRST MIGRATION
/* MIGRATION TO ADD USER SETTINGS
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
* This migration adds the table `user_setting` in order to store all sorts of user configuration data
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {

View File

@ -1,15 +1,11 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* FIRST MIGRATION
/* MIGRATION TO CREATE THE LOGIN_SERVER TABLES
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
* This migration creates the `login_server` tables in the `community_server` database (`gradido_community`).
* This is done to keep all data in the same place and is to be understood in conjunction with the next migration
* `0004-login_server_data` which will fill the tables with the existing data
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {

View File

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* FIRST MIGRATION
/* MIGRATION TO COPY LOGIN_SERVER DATA
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
* This migration copies all existing data from the `login_server` database (`gradido_login`)
* to the `community_server` database (`gradido_community`) in case the login_server database
* is present.
*
* NOTE: This will fail if the two databases are located on different servers.
* Manual export and import of the database will be required then.
* NOTE: This migration does not delete the data when downgrading!
*/
const LOGIN_SERVER_DB = 'gradido_login'

View File

@ -1,12 +1,6 @@
/* MIGRATION FOR ADMIN INTERFACE
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
* This migration adds the table `login_pending_tasks_admin` to store pending creations
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {

View File

@ -0,0 +1,16 @@
/* MIGRATION TO ALIGN COLLATIONS
*
* in oder to be able to compare `login_users` with `state_users`
* when the databases default is not `utf8mb4_unicode_ci`, we need
* to also explicitly define it in the table
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4;')
}

View File

@ -10,9 +10,9 @@
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"up": "cd build && node src/index.js up",
"down": "cd build && node src/index.js down",
"reset": "cd build && node src/index.js reset",
"up": "node build/src/index.js up",
"down": "node build/src/index.js down",
"reset": "node build/src/index.js reset",
"dev_up": "ts-node src/index.ts up",
"dev_down": "ts-node src/index.ts down",
"dev_reset": "ts-node src/index.ts reset",

View File

@ -13,7 +13,6 @@ const database = {
const migrations = {
MIGRATIONS_TABLE: process.env.MIGRATIONS_TABLE || 'migrations',
MIGRATIONS_DIRECTORY: process.env.MIGRATIONS_DIRECTORY || './migrations/',
}
const CONFIG = { ...database, ...migrations }

34
database/src/helpers.ts Normal file
View File

@ -0,0 +1,34 @@
import CONFIG from './config'
import { createPool, PoolConfig } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import path from 'path'
const poolConfig: PoolConfig = {
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
}
// Pool?
const pool = createPool(poolConfig)
// Create & Initialize Migrations
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
silent: true,
dir: path.join(__dirname, '..', 'migrations'),
})
const initialize = async (): Promise<void> => {
await migration.initialize()
}
const resetDB = async (closePool = false): Promise<void> => {
await migration.reset() // use for resetting database
if (closePool) pool.end()
}
export { resetDB, pool, migration, initialize }

View File

@ -1,7 +1,4 @@
import 'reflect-metadata'
import { createPool, PoolConfig } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import CONFIG from './config'
import prepare from './prepare'
import connection from './typeorm/connection'
import { useSeeding, runSeeder } from 'typeorm-seeding'
@ -10,30 +7,12 @@ import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed'
import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed'
import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed'
import { DecayStartBlockSeed } from './seeds/decay-start-block.seed'
import { resetDB, pool, migration } from './helpers'
const run = async (command: string) => {
// Database actions not supported by our migration library
await prepare()
// Database connection for Migrations
const poolConfig: PoolConfig = {
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
}
// Pool?
const pool = createPool(poolConfig)
// Create & Initialize Migrations
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
dir: CONFIG.MIGRATIONS_DIRECTORY,
})
// Database connection for TypeORM
const con = await connection()
if (!con || !con.isConnected) {
@ -52,7 +31,7 @@ const run = async (command: string) => {
break
case 'reset':
// TODO protect from production
await migration.reset() // use for resetting database
await resetDB() // use for resetting database
break
case 'seed':
// TODO protect from production

View File

@ -2,25 +2,39 @@
Benutzer
* *
* natürliche Person = TRUE (Flag für Schöpfungserlaubnis Human)
* Vorname
* Nachname
* natürliche Person = FALSE (Flag für Schöpfungserlaubnis Human)
* Projekt/Firmenname/Organisation/Verein...
* Benutzername
* Emailadresse
* Konto
* Trustlevel (Zukunft)
* natürliche Person = TRUE (Flag für Schöpfungserlaubnis Human)
* Vorname
* Nachname
* natürliche Person = FALSE (Flag für Schöpfungserlaubnis Human)
Gradido-ID als Ersatz für Email-Adresse : server/nutzername
* Projekt/Firmenname/Organisation/Verein...
* Benutzername / useralias
* Emailadresse
* Konto
* Trustlevel (Zukunft)
## Gradido-ID
Die GradidoID oder auch Username dient zur eindeutigen Identifizierung eines Nutzers. Sie soll einerseits *human readable* , aber auch die maschinelle Weiterverarbeitung bzw. Nutzung ermöglichen. Daher werden folgende Regeln festgelegt:
Pattern: `<communityname>`/`<useralias>`
* `<communityname>`
- Zeichen, die in einer URL erlaubt sind:
* A-Z, a-z, 0-9
* '-', '.', '_', '~'
* `<useralias>`:
- mindestens 5 Zeichen
* alphanumerisch
* keine Umlaute
* - und \_ sind nur zwischen sonst gültigen Zeichen erlaubt (RegEx: [a-zA-Z0-9]-|_[a-zA-Z0-9])
- Blacklist für Schlüsselworte, die frei definiert werden können
- vordefinierte/reservierte System relevante Namen dürfen maximal aus 4 Zeichen bestehen
Nutzername ist pro server eindeutig
Nutzerprofil mit Bild und persönlichen Angeboten
## Anwendungsfälle
### neuen Benutzer anlegen

View File

@ -0,0 +1,137 @@
# Betrieb und Support
Dieses Dokument beschreibt die Anforderungen für den Betrieb und Support der Gradido-Anwendung während der Entwicklungsphase. Diese gelten nicht in vollem Umfang für den Betrieb der Gradido-Anwendung für eine neue Community, die sich rein auf eine Produktiv-Umgebung reduzieren läßt.
# Umgebungen
Für die Phase der Entwicklung werden folgende Stages benötigt:
* LOKAL-Stage: Entwicklungsumgebung für einen Entwickler
* DEV-Stage: zentrale Entwicklungsumgebung für erste Integrationsschritte aus dem Entwicklerteam
* INT-Stage: zentrale, produktionsnahe Integrationsumgebung zum Aufbau und Test eines neuen Release
* PROD-Stage: zentrale Produktiv-Umgebung für den Betrieb mindestens einer Community
## LOKAL-Stage
Die lokale Entwicklungsumgebung liegt in dem Verantwortungsbereich eines jeden Entwicklers. Es müssen dabei für die Entwicklung im Gradido-Team alle notwendigen Tools und Komponenten installiert bzw. vorhanden sein.
### Resources
### Betriebesystem
### Tools
### Komponenten und Dienste
## DEV-Stage
### Resources
CPU
RAM
Storage
Network/Traffic
Backup
### Betriebesystem
### Tools
### Komponenten und Dienste
## INT-Stage
### Resources
CPU
RAM
Storage
Network/Traffic
Backup
### Betriebesystem
### Tools
### Komponenten und Dienste
## PROD-Stage
### Resources
CPU
RAM
Storage
Network/Traffic
Backup
### Betriebesystem
### Tools
### Komponenten und Dienste
#### Security
#### Backup und Restore
tägliches Backup
#### Redundanz und Failover
Virtulisierung
Balancing
# Provider
Für das Hosting der Stages DEV, INT, PROD wird ein Provider gesucht, der die gewünschten Anforderungen als Dienstleister zu einem angemessenen Preis und mit seriösem Hintergrund anbietet. Es sollen hier verschiedenen Provider aufgelistet und ihre Leistungen und Kosten gegenübergestellt werden, so dass eine Entscheidungsfindung für eine Beauftragung möglich wird.
## aktuell beauftragte Provider und Dienstleister
Derzeit (10.2021) sind Leistungen und Dienste von folgenden Providern gebucht:
### Elopage
Forum, Mitglieder-Pakete, Spenden
### Klicktipp
Newsletter
### ebiz
Marktplatz
### 1fire
Webhoster PROD
ohne Backup
### strato
Webhoster DEV und INT

View File

@ -4,66 +4,119 @@ Diese Konzept beschreibt den Begriff "Community" im Kontext von Gradido, welche
## Die Bedeutung des Begriffs Community
Eine Community bedeutet im Kontext von Gradido eine Gemeinschaft von Personen, die sich nach der Philosophie von Gradido zu einer gemeinsamen Gruppierung zusammenschließen. Unter dem gemeinsamen Zusammenschluß folgen sie der Natürlichen Ökonomie des Lebens. Die Community dient dabei als Rahmen für die Gruppe von Personen, um ihnen den geregelten Zugang zu ermöglichen. Unter dem Begriff "Zugang zur Community" wird die Registrierung eines Benutzerkontos für eine Person verstanden. Dabei erfolgt eine Autentifizierung der Person, um einen personenspezifischen Zugriff auf die Community-Funktionalitäten zu ermöglichen. Denn eine Community bietet einer Person eine Vielzahl an Funktionalitäten, die ein Community-Mitglied nutzen kann. So steht die Verwaltung und das Handeln mit Gradido-Geld als die Hauptfunktionalität einer Community im Vordergrund. Doch sind auch weitere Funktionalitäten, wie eine Selbstdarstellung über Benutzerprofile oder ein sich Vernetzen mit Community-Mitgliedern, aber auch ein Community übergreifendes Vernetzen als soziale Netzewerke möglich. So können aus kleinen Communities über Vertrauensverhältnisse Zusammenschlüsse mehrere eigenständigen Communities entstehen oder auch eine Hierarchie von Communities als Parent-Child-Verbindung aufgebaut werden (siehe weiter unten "Community-Modelle").
Eine Community bedeutet im Kontext von Gradido eine Gemeinschaft von Personen, die sich nach der Philosophie von Gradido zu einer gemeinsamen Gruppierung zusammenschließen. Unter dem gemeinsamen Zusammenschluß folgen sie also einerseits gemäß der Gradido-Philosophie der *Natürlichen Ökonomie des Lebens* und andererseits ihrer ursprünglichen Idee eine Gemeinschaft zu bilden.
Innerhalb der Community erfolgt die Umsetzung und Verwaltung des "lebendigen Geldes". Soll heißen hier werden die Mechanismen zur Dreifachen-Schöpfung vollzogen, die das geschöpfte Geld nach den Community-Regeln auf die drei Arten von Empfängerkonten (Benutzerkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto) verteilt. Ein Community-Mitglied kann über seinen Community-Zugang auf sein persönliches Benutzerkonto zugreifen und darüber sein Gradido-Geld verwalten. Neben der Einsicht auf seinen aktuellen Kontostand kann er u.a. seine regelmäßig geschöpften Gradido einsehen, mit vorhandenen Gradido bezahlen oder einem anderen Mitglied Gradido überweisen. Die Geldbewegungen werden als eine Liste von Transaktionen geführt und die Vergänglichkeit der Gradidos immer aktuell zur Anzeige gebracht.
Als Gradido System-Komponente beinhaltet die *Community* die grundlegenden Funktionalitäten und Prozesse zur Verwaltung der Gruppenmitglieder, ihrer Registrierungs- und Systemzugriffe, die Konten- und Geldverwaltung einerseits und andererseits die Funktionalitäten und Prozesse zur Vernetzung und Kommunikation von mehreren Communities untereinander .
Innerhalb der Community-Komponente erfolgt die Umsetzung und Verwaltung des *lebendigen Geldes*. Soll heißen hier werden die Mechanismen zur Dreifachen-Schöpfung vollzogen, die das geschöpfte Geld nach den Community-Regeln auf die drei Arten von Empfängerkonten (AktivesGrundeinkommenkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto) verteilt. Ein Community-Mitglied kann über seinen Community-Zugang auf sein persönliches Benutzerkonto zugreifen und darüber sein Gradido-Geld verwalten. Neben der Einsicht auf seinen aktuellen Kontostand kann er u.a. seine regelmäßig geschöpften Gradido einsehen, mit vorhandenen Gradido bezahlen oder einem anderen Mitglied Gradido überweisen. Die Geldbewegungen werden als eine Liste von Transaktionen geführt und die Vergänglichkeit der Gradidos immer aktuell zur Anzeige gebracht.
Nach der Bedeutung des Begriffs Community werden nun die Eigenschaften einer Community detailliert beschrieben, damit all die zuvor erwähnten Möglichkeiten der Community abbildbar sind.
## Eigenschaften einer Community
Hier werden die Eigenschaften einer Community beschrieben, die notwendig sind, um die oben erwähnten Möglichkeiten der Community zu erfüllen. Es geht dabei um verschiedene Themen und ihre dazu notwendigen Prozesse, die wiederum unter Verweiß in anderen Dokumenten detailter beschrieben sind.
Hier werden die Eigenschaften einer Community beschrieben, die notwendig sind, um die oben erwähnten Möglichkeiten der Komponente zu erfüllen. Es geht dabei um verschiedene Themen und ihre dazu notwendigen Prozesse, die wiederum unter Verweiß in anderen Dokumenten detailter beschrieben sind.
### Währung
In einer Community werden die Prozesse der 3-fachen-Geldschöpfung, sowie der Transfer von Geld in der *Gradido-Währung* ablaufen. Mit dem Erstellen einer neuen Community wird technisch gesehen gleichzeitig auch eine eigene *Community-Gradido* Währung bei der Schöpfung erzeugt.
Ziel dieser Community eigenen Währung ist für die Gemeinschaft über ein Währungs-Branding sich marketingstechnisch hervorheben zu können. Zum Beispiel könnte eine Community aus der Region "Liebliches Taubertal" sich über den *Community-Gradido* den sogenannten "Taubertäler" erzeugen, den die Mitglieder dann aber auch überregional mit anderen Communities in Umlauf bringen und somit Werbung für ihre Region machen.
| PR-Kommentare | |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Was hälst du von einem Namen für die Währung "Der Taubertäler", den jede Community definieren kann, sich aber auch zu einer anderen anschließen kann. Dies ist aber nur das Branding in Sachen Anzeige & Marketing. Darunter liegt der Gradido - dieser ist weiter aufgestellt als nur regional - nämlich weltweit. Auch hier müssen "Bösewichte" ausgeschlossen werden können - Prozess & Regeln dafür?<br /><br />Ich mache mir einfach ein wenig Sorgen, dass wir die Anforderung "Jeder Coin hat eine eindeutige Community-Prägung" nicht erfüllen können mit der aktuellen Implementierung und ich habe auch Schwierigkeiten mir vorzustellen, wie sich das mit Schwundgeld verhalten soll - insbesondere mit stärker schwindendem Geld bei größerer Geldmenge. Der Schwund ist also auf alle meine Coins gebunden, währen die Prägung auf den jeweiligen Coin passiert. Ich glaube das ist sehr schwierig beides entsprechend abzubilden. |
| Claus-Peter 25.11.2021 | Ich kann die Bedenken einerseits verstehen und andererseits die Visionen von Bernd nachvollziehen. Dass diese nicht immer einfach unter einen Hut zu bringen sind, ist klar und dennoch möchte ich diese zumindest in den fachlichen Anforderungen nicht einfach rausstreichen.<br />Wie diese dann technisch zu konzipieren sind ist und bleibt letztendlich genau unsere Aufgabe. |
Andererseits soll aber, wenn eine Community sich bei der Geldschöpfung nicht an die Regel der *Gradido-Philosopie* hält, eine technische Möglichkeit geschaffen sein, dass diese Community in ihrer weiteren Geldschöpfung und dem Handel *ihrer* Währung sanktioniert werden kann.
Aber grundsätzlich bleibt bei allen *Community-Gradido*-Währungen die Vergänglichkeit als Sicherungsmechanismus des Geldvolumens und der 1:1 Umtausch zwischen verschiedenen *Community-Gradidos* bestehen.
#### Schutz vor Falschgeld
- Blacklisting
- Bereinigung durch Bezahlen nach Priorisierung
- 1. GDD von der Community des Empfängers
2. GDD von anderen Communities nach Menge von wenig nach viel
3. GDD von der eigenen Community
4. geblacklistete werden gar nicht verwendet und vergehen
* Vergänglichkeitsbereinigung
* 1. GDD anderer Communities nach Menge von wenig nach viel
| PR-Kommentare | |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Wenn die geblacklisteten Coins prioritisiert vergehen kann der findige Angreifer den Verfall umgehen, indem er sich ungültige/blacklistete Coins erschafft, die dann genau seinem Verfall entsprechen. |
| Claus-Peter 25.11.2021 | Das Kapitel "Schutz vor Falschgeld" ist wohl eher noch im Status "Brainstorming" zu verstehen. Hier wurden mögliche Regeln notiert, die noch nicht in ihrer Gänze durchdacht und konzipiert sind.<br />Der Punkt Vergänglichkeitsbereinigung sagt folgendes aus:<br />Kontostand am 01.01.2021:<br />- 100 GDD aus der eigenen Community<br />- 100 GDD aus Community A<br />- 100 GDD aus Community B<br />- gesamt 300 GDD<br /><br />Nach einem Jahr ohne irgendwelche weiteren Transaktionen ergibt sich folgendes am 31.12.2021:<br />- Vergänglichkeit mit 365 Tagen bei 300 GDD = 150 GDD (nicht gerechnet, sondern 50%)<br />- die 150 GDD Vergänglichkeit als Tx-Buchung führt zu folgendem Kontostand:<br /> * 100 GDD aus der eigenen Community<br /> * 50 GDD aus der Community A<br /> * 0 GDD aus der Community B<br /> * gesamt 150 GDD<br /><br />Soweit der Gedankengang zur Bereinigung des Kontos mit GDD aus anderen Communities. Das hat keine Auswirkung auf die Wertigkeit, sondern soll sich allein auf die Reduktion der Viefältigkeit an Community-Währungen im eigenen Konto führen. |
* Bezahl-Vorbereitung
* Austausch von Blacklist zw. Teilnehmer
* ggf. Übersteuern der Balcklist falls gewünscht
| PR-Kommentare | |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Ich denke die vervielfachung von Coins und damit eine Ab/Auf-Wertung der jeweiligen Währen einer Community ist entgegen dem Konzept von Gradido. Gradido schafft eine stabile Zeit-Tausch-Einheit. Diese sollte Weltweit den gleichen Wert haben - warum sollte der Peruaner für seine Zeit weniger Gradido bekommen als ein Europäer? Das zementiert einfach weiterhin die bestehende Ordnung auf dem Planeten. Wollen wir das?<br />Daher mein Credo: Niemals einen Faktor zwischen Communities einführen. |
| Claus-Peter 25.11.2021 | Den Kommentar verstehe ich nicht in Bezug auf die zitierten Dokument-Zeilen.<br />Wo finden sich Hinweise auf eine Ab/Auf-Wertung der Währung?<br />Das Blacklisting ist mit Sicherheit ein sehr sensibler Punkt und muss genauestens und tiefer durchdacht und konzipiert werden.<br />Ansonsten stimme ich dem Inhalt und deinem Credo voll zu. |
### Anzeige und -Darstellung
Da es also mehrere Communities geben wird, benötigt jede Community ihren eigenen Namen und gar ein Symbol oder Bild, um eine optische Unterscheidung bei der Anzeige in den Systemen sicherzustellen. Für eine Aussendarstellung wäre eine Beschreibung der Community und ihre eigene Philosopie, was die Community auszeichnet hilfreich. Diese Werte müssen vom Community-Administrator gepflegt werden können.
Da es also mehrere Communities geben wird, benötigt jede Community ihren eigenen Namen und gar ein Symbol oder Bild, um eine optische Unterscheidung oder gar eigenes Branding bei der Anzeige in den Systemen sicherzustellen. Für eine Aussendarstellung wäre eine Beschreibung der Community und ihre eigene Philosopie, was die Community auszeichnet hilfreich. Diese Werte müssen vom Community-Administrator gepflegt werden können.
### Mitgliederverwaltung
Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteil durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft. Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](.\BenutzerVerwaltung.md).
Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteile durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft.
### Community-Vernetzung
Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](.\BenutzerVerwaltung.md).
Für die Community-Vernetzung sind Verwaltungsprozesse zwischen den Communities und auch den Community-Mitgliedern notwendig, um entsprechende Vertrauensverhältnisse aufzubauen. Diese müssen den notwendigen Sicherheitsansprüchen genügen, da darauf aufbauend dann später die Geld-Flüsse abgewickelt werden. Entsprechend den Community-Modellen (siehe im folgenden Unterkapitel **Community Modelle**) wird ein Prozess benötigt, der die Hierarchie bzw. das Vertrauensverhältnis zwischen zwei eigenständigen Communities aufbaut und daraus dann die möglichen Funktionalitätserweiterungen für die Mitglieder bzw. den Communities freischaltet bzw. unterstützt. Zusätzlich wird auch der jeweilige umgekehrte Prozess benötigt, der eine bestehende Hierarchie bzw. ein bestehendes Vertrauensverhältnis zwischen zwei Communities auflöst und löscht, sowie die daraus resultierenden Funktionseinschränkungen für die Mitglieder und die betroffenen Communities.
### Community-Netzwerk
Zum besseren Verständnis der Community-Vernetzung erfolgt hier eine Beschreibung der möglichen Konstellationen, wie sich Communities miteinander verbinden können.
Ein grundlegender Ansatz des Gradido-Systems beinhaltet die Einstufung aller beteiligten Gradido-Communities als gleichberechtigte Einheiten. Diese bilden unterneinander ein Kommunikations-Netzwerk zum Austausch an Informationen, aber auch zum Aufbau eines gemeinsamen Verbundes weiterer Aktivitäten.
#### Community Modelle
#### Vernetzung
Bei Gradido werden verschiedene Modelle von Community-Abhängigkeiten unterstützt. Dabei soll unterschieden werden zwischen:
Die Vernetzung der Gradido-Communities erfolgt automatisch über eine Channel-Infrastruktur.
* eigenständige Community
* sich gegenseitig vertrauende Communities
* von einander abhängige (vererbende) Communities
* Mischung aus den vorherigen Modellen
![CommunityCreationChannel](./image/CommunityCreationChannel.png)
Das nachfolgende Bild zeigt einen ersten Eindruck über die unterschiedlichen Community-Modelle:
Das bedeutet mit dem Aufsetzen und Inbetriebnehmen einer neuen Community erfolgt eine automatisierte Vernetzung der neuen Community mit den schon existierenden Communities über einen dedizierten Kommunikationskanal. Dies dient in aller erster Linie dazu, dass sich alle Gradido-Communities untereinander kennen lernen. Das dadurch entstehende Netzwerk von Gradido-Communities benötigt somit keinen zentralen Knoten, der die Verwaltung der dem Netzwerk beigetretenen Instanzen übernehmen müsste.
![CommunityModell](./image/CommunityModell.png)
![CommunityNetwork](./image/CommunityNetwork.png)
##### Eigenständige Community
Alle späteren Aktivitäten wie u.a. das gemeinsame Handeln oder Gradido-Transfer erfolgen dann in direkter Kommunikation zwischen zwei Communities. Dabei lauschen die Communities an nach fachlichen Themen separierte Kommunikationskanäle. Sobald eine direkt an eine Community adressierte oder auch wenn eine für eine Community interessante Nachricht empfangen wird, erfolgt die weitere Verarbeitung dieser Nachricht in direktem Austausch der beiden betroffenen Communities. Durch die Teilnahme einer Community an spezifischen fachlichen Kommunikationskanäle lernen sich die Communities untereinander an ihren spezifischen Interessen besser kennen bzw. können auch durch aktives Propagieren die engere Vernetzung zwischen den Communities beschleunigen.
Eine eigenständige Community zeichnet sich darin aus, dass sie keine Beziehung zu einer anderen Community aufgebaut hat. Das heißt sie hat weder eine vertrauenswürdige Verknüpfung mit einer zweiten Community, noch hat sie eine Verbindung zu einer Parent-Community und besitzt auch selbst keine Verbindung zu einer Child-Community. Somit kann diese Community für ihre Mitglieder nur Community intern wirksame Prozesse anbieten. Das heißt es ist kein Community übergreifender Handel bzw. Austausch von Gradido möglich. Andererseits werden in dieser Community die Prozesse freigeschaltet, dass ein Aufbau eines vertrauenswürdiges Verhältnis zu einer anderen Community erlaubt, der Aufbau einer Parent-Beziehung und auch der Aufbau einer Child-Beziehung ermöglicht. Die zugehörigen Abbau-Prozesse dagegen sind nicht freigeschalten. Der Community übegreifende Überprüfungsprozess bei der Mitglieder-Registrierung zur eindeutigen Identifikation in der Mitglieder-Verwaltung zählt dabei nicht als vertrauenswürdige Verbindung zwischen Communities.
![CommunityChannelCommunication](./image/CommunityChannelCommunication.png)
##### Gegenseitig vertrauende Communities
#### Ausfallsicherheit
*Hier soll beschrieben werden, was den Unterschied auszeichnet zu einer "Eigenständigen Community", wie man das gegenseitige Vertrauen (sprich Verknüpfung) zwischen zwei oder mehreren Communities auf- und wieder abbaut, was bedarf es an Vorraussetzungen für einen Vertrauens-Auf/Abbau und welche Konsequenzen der Auf- und Abbau des gegenseitigen Vertrauens haben soll.*
Ein weiterer wichtiger Aspekt der Community-Vernetzung ist die Sicherstellung der Ausfallsicherheit. Dabei erfolgt im Community-Netzwerk die Verteilung von Community eigenen Daten auf Knoten anderer Communities. Dadurch kann jederzeit bei einem Ausfall eines Netzwerkknotens und den damit betroffenen Communities einerseits ein online Fail-Over-Szenario betrieben werden und/oder andererseits der Wiederaufbau eines neuen Knotens mit den verlorenen Community-Daten und aus dem Netzwerk wiederhergestellten Daten erfolgen.
Das Modell der sich *gegenseitig vertrauenden Communities* entspringt der Idee des sich miteinander Vernetzens und damit das Handeln und Agieren mit Gradido-Mitgliedern, die nicht in der eigenen Community als Mitglied registriert sind. Um dies zu ermöglichen bedarf es einem Aufbau-Prozess zwischen zwei Communities, die sich zukünftig gegenseitig ein enges Vertrauen schenken. Auf der Basis dieses Vertrauens tauschen die beiden Communities Informationen untereinander aus, so dass für die Mitglieder beider Communities die Funktionalitäten auf der Gradido-Plattform so transparent erscheinen, als ob sie alle Mitglied einer Community wären. Das würde sich beispielsweise bei der Suche nach einem bestimmten Community-Mitglied auswirken, da nun alle Mitgleider beider Communities in einer Liste zur Anzeige gebracht werden können. Oder der Transfer von Gradidos von einem Mitglied zu einem anderen Mitglied ist über dieses Community-Verhältnis nun auch Community übergreifend möglich. Auch weitere Angebote, die bisher nur in einer Community zur Verfügung standen, sind nun auch den Mitgliedern der anderen Community zugänglich.
#### Eindeutige Mitgliedschaft
Während des Aufbau-Prozesses werden neben den eigentlichen Security relevanten Informationen für den Aufbau und die Sicherstellung des Vertrauensverhältnisses auch fachliche Informationen ausgetauscht. Unter fachlichen Informationen sind die nun freigeschaltenen Angebote beider Communities gemeint. Somit werden in der einen Community nun auch die fachlichen Prozesse und Angebote der anderen Community zugänglich und freigeschalten und umgekehrt. Wie feingranular die Prozesse und Angebote dabei ausgetauscht und freigeschaltet werden unterliegt einer administrativen Konfiguration der jeweiligen Community. Das heißt der Administrator jeder Community kann im Vorfeld selektiv konfigurieren welche Angebote und Prozesse beim Aufbau-Prozess für ein Vertrauensverhältnis mit einer anderen Community übertragen und freigeschaltet werden. Diese Konfiguration sollte zuvor Community intern abgestimmt sein, um nicht schon zu Beginn der Zusammenarbeit der beiden Communities irgendwelche Missstimmungen unter den Mitgliedern zu verursachen. Die Details des *Vertrauensverhältnis Aufbau-Prozesses* sind weiter unten im Kapitel **Anwendungsfälle** beschrieben.
Durch das Community-Netzwerk erfolgt auch der sehr wichtige Prozess der Sicherstellung, dass eine natürliche Person sich nur einmal bei einer Community im gesamten Community-Netzwerk registrieren darf. Dazu erfolgt ein Informationsaustausch über einen bestimmten Kommunikationskanal zwischen allen Communities untereinander. Das dazu notwendige Protokoll und die benötigten Daten werden im technischen Konzept definiert. Die Entscheidung, ob die Überprüfung der eindeutigen Mitgliedschaft direkt mit dem eigentlichen Registrierungsprozess eines Mitglieds gekoppelt werden kann oder ob diese nachträglich asynchron im Hintergrund stattfinden muss, findet erst bei der technischen Konzeption ggf. durch ein technisches Proof-of-Concept statt.
##### Abhängige Communities
| PR-Kommentar | |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Diese Anforderung ist technisch nicht zu erfüllen.<br />1. Eine Identifikation einer Person erfolgt immer mithilfe eines technischem Identifiers wie z.B. der Personalausweisnummer, EMail, Telefonnummer oder anderes. Schon durch diese Abstraktion kann ein Nutzer mehr als eine Identität aufbauen (2 Telefonnummer z.B.)<br />2. Die Prüfung auf Eindeutigkeit in einem dezentralen Netzwerk kann nicht sichergestellt werden, da Teile des Netzwerks zum Zeitpunkt der Prüfung nicht erreichbar oder gar unbekannt sein können.<br />3. Bedarf es den Austausch der Personen-Identifikation zwischen dem Communties: "kennst du email@domain.com?". Diese Daten können verschlüsselt werden z.B mit `hash(salt,email)` welche dann an jede Community geschickt werden: kennst du `hash(salt,email), salt`? Was dazu führt, dass jede Community den hash aller seiner EMails errechen muss - die Skalierung ist entsprechend schlecht.<br /><br />Alternativ kann hier eine Blockchain eingesetzt werden, welche `hash(salt,email), salt` speichert und als dezentrales Nachschlagewerk für alle zugänglich ist. Hier erwarte ich ein Konzept, bevor wir das umsetzen können. Die Sicherheit der Nutzerdaten ist ebenfalls genau zu untersuchen, wenn wir das ganze ins Internet blasen. |
| Claus-Peter 25.11.2021 | Ja da gebe ich dir nach heutigem Stand vollkommen Recht, dass es dafür derzeit keine 100% technische Lösung gibt.<br />Trotzdem ist das Thema fachlich gewünscht und wir müssen uns Gedanken machen, wie wir dafür eine technische Lösung finden können, die nahezu an die Anforderungen heranreicht. Genau deshalb endet dieser Absatz mit dem Hinweis auf die technsiche Konzeption. |
*Hier soll beschrieben werden, was den Unterschied zu eigenständigen und sich gegenseitig vertrauenden Communities zu den hier abhängigen (sprich vererbten) Communities auszeichnet, welche Vorraussetzungen bedarf der Auf/Abbau einer abhängigen Community und welche Konsequenzen hat der Auf- und Abbau von abhängigen Communities.*
Das Modell der *abhängigen Communities* findet seinen Ursprung den Föderalismus von Deutschland in einer Community-Struktur abbilden zu können. Das bedeutet, dass eine baumartige Struktur von Communities aufgebaut werden kann, wie nachfolgendes Bild schemenhaft zeigt:
### Hirarchische Community
Um die Vision Gradido als Währung nicht nur in Communities als gemeinsame Interessensgemeinschaften zu etablieren, sondern auch für ganze Communen, Bundesländer, Nationen oder gar weltweit, bedarf es einer Strukturierung von Communities. Dazu dient das Konzept der *hierarchischen Community*, seinen Ursprung in der Abbildung des Föderalismus von Deutschland findet. Das bedeutet, dass eine baumartige Struktur von Communities aufgebaut werden kann, wie nachfolgendes Bild schemenhaft zeigt:
| PR-Kommentar | |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 25.11.2021 | Ich denke Förderalismus wie in der Bundesrepublik ist mit Gradido nicht möglich. Es ergibt sich durch das Schwundgeld einfach keine Vorteile eines Förderalismus.<br />Szenario Straßenbau zwischen zwei Communties:<br />- Warum sollte Geld von beiden Communities auf ein drittes Konto fließen, wenn es dort doch nur vergeht?<br />- Ist es nicht realistischer, dass beide Communties sich auf den Straßenbau einigen und das Geld direkt an den Auftragnehmer überweisen, um dem Schwund so weit es geht zu entgehen.<br /><br />Warum sollte sich eine Community einer anderen unterordnen? Was sind die Vorteile?<br />*Nur ein Gedanke* |
| Claus-Peter 25.11.2021 | Ich kann deinen Gedanken und Bedenken folgen, doch andererseits kann ich auch dem Föderalismus etwas abgewinnen.<br />Klar würde sich eine Community schwer tun, sich einer anderen Community unterzuordnen. Doch genau da beginnt die Überlegung, wie man ein Community-übergreifendes Projekt organisieren könnte? Da gibt es schon Vorteile, die natürlich noch feiner konzipiert und ausformuliert werden müssen. Daher sollten wir die Hierarchie von Communities nicht im Vorhinein ausschließen. |
![hierarchisches Community-Modell](./image/HierarchischesCommunityModell.png)
Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Child-Beziehung erzeugt. Dadurch gehen diese beiden Communities eine besondere Beziehung untereinander ein, die zu folgenden veränderten Eigenschaften und Verhalten der Parent- und der Child-Community führen:
Es wird somit zwischen zwei Communities aus direkt benachbarten Ebenen eine Parent-Child-Beziehung erzeugt. Dadurch gehen diese beiden Communities eine besondere Beziehung untereinander ein, die zu folgenden veränderten Eigenschaften und Verhalten der Parent- und der Child-Community führen:
###### Parent-Community
#### Parent-Community
* kann 1 bis n Child-Communities besitzen
* verwaltet keine Mitglieder mit AGE-Konto
@ -73,14 +126,14 @@ Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Chil
* bedarf spezieller Administrationsprozesse zur Verwaltung der Parent-Aufgaben:
* Auf- und Abbau der Parent-Child-Beziehung
* Verschiebung aller Mitglieder von der Parent- in die Child-Community
* Stoppen des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die erste Child-Beziehung aufgebaut ist und alle Mitglieder dahin verschoben sind
* Stoppen des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die erste Child-Beziehung aufgebaut ist und alle Mitglieder dorthin verschoben sind
* Prozess zur Aufnahme der geschöpften Allgemeinwohl- und AUF-Gelder aus den Child-Communities
* stoppt den Schöpfungsprozess sobald eine Child-Beziehung aufgebaut ist
* startet den Schöpfungsprozess sobald die letzte Child-Beziehung aufgelöst ist
* Aufnahmeprozess von Mitgliedern aus einer Child-Community, bevor dessen Beziehung aufgelöst wird
* starten des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die letzte Child-Beziehung aufgelöst ist
###### Child-Community
#### Child-Community
* besitzt genau eine Parent-Community
* **sofern es eine Community der untersten Ebene ist:**
@ -94,32 +147,38 @@ Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Chil
* hier läuft der Prozess zur Sicherstellung, dass eine *natürliche Person* nur Mitglied einer einzigen (Child)-Community ist
*
##### Mischung aus den vorherigen Modellen
*Hier soll beschrieben werden welche möglichen Mischungen von Modellen erlaubt sind und welche nicht, was hat eine Mischungsvariante an Konsequenzen, wie wird eine Mischungsvariante auf/abgebaut, welche Vorraussetzungen bedarf es für den Auf/Abbau einer Mischungsvariante.**
### Geldschöpfung
Eine Community stellt die Mechanismen für die Dreifache-Geldschöpfung bereit. Dazu müssen zuerst die Verteilungsschlüssel auf die drei Kontoarten definiert bzw. konfigurierbar sein. Diese Konfigurationswerte werden vom Community-Administrator gepflegt. Sie dienen als Grundlage für die Höhe der regelmäßig geschöpften Beträge auf die drei Empfängerkonto-Typen. Die regelmäßige Geldschöpfung läuft automatisiert im Hintergrund und muss den Regeln der Nartürlichen Ökonomie des Lebens folgen. Die Details der Dreifachen Geldschöpfung sind in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) beschrieben.
Eine Community stellt die Mechanismen für die Dreifache-Geldschöpfung bereit. Dazu müssen zuerst die Verteilungsschlüssel auf die drei Kontoarten definiert bzw. konfigurierbar sein. Diese Konfigurationswerte werden vom Community-Administrator gepflegt. Sie dienen als Grundlage für die Höhe der regelmäßig geschöpften Beträge auf die drei Empfängerkonto-Typen. Die regelmäßige Geldschöpfung läuft teilweise automatisiert im Hintergrund und muss den Regeln der Nartürlichen Ökonomie des Lebens folgen. Die Details der Dreifachen Geldschöpfung sind in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) beschrieben.
### Konto-Verwaltung
Durch die Dreifach-Geldschöpfung verwaltet die Community auch die drei Arten von Konten: Benutzerkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto(AUF).
Für die Dreifach-Geldschöpfung verwaltet die Community drei Arten von Konten: das AktiveGrundeinkommen-Konto pro Mitglied, das Community-eigene Gemeinwohlkonto und das Community-eigene Ausgleichs- und Umweltkonto(AUF).
Für jedes Mitglied der Community wird also ein eigenes Benutzerkonto verwaltet, auf das ein Drittel der monatlichen Geldschöpfung fließt. Das Gemeinwohlkonto und das AUF-Konto existieren pro Community einmal und auf jedes der beiden Konten fließen monatlich die beiden anderen Drittel der Geldschöpfung.
Für jedes Mitglied der Community wird also ein eigenes AktiveGrundeinkommen-Konto verwaltet, auf das ein Drittel der monatlichen Geldschöpfung unter Einhaltung der AGE-Regeln fließt. Das Gemeinwohlkonto und das AUF-Konto existieren pro Community einmal und auf jedes der beiden Konten fließen monatlich die beiden anderen Drittel der Geldschöpfung.
Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das Benutzerkonto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen Benutzerkonten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](.\KontenVerwaltung.md) beschrieben.
Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das AGE-Konto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen AGE-Konten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](.\KontenVerwaltung.md) beschrieben.
### Tätigkeitsverwaltung
Hier handelt es sich um eine Verwaltung von Tätigkeitsbeschreibungen, die von den Community-Mitgliedern als akzeptierte und berechtigte Leistungen zur Geldschöpfung als *Aktives Grundeinkommen* angesehen werden. Das heißt die Community muss unter den Mitgliedern eine Liste erarbeiten, die alle Tätigkeiten enthält, aus denen sich ein Mitglied dann eine oder mehrere auswählen kann, um sich sein Aktives Grundeinkommen damit zu decken. Die einzelnen Tätigkeiten sollen auch fachlich strukturierbar sein z.B. Kunst, Soziales, Gesundheit, Produktion, etc. . Die Menge und Definition der einzelnen Tätigkeiten und Strukturen unterliegt einer stetigen Anpassung nach den Bedürfnissen der Community-Mitglieder, um den natürlichen Veränderungen des miteinander Lebens gerecht werden zu können. Ob zu einer Tätigkeitsbeschreibung auch gleich eine Wertigkeit definiert werden soll, ist noch offen. Man kann aber sicherlich sagen, dass manche Tätigkeiten dem Gemeinwohl dienlicher sind als andere. Aber auch das ist wiederum eine Ansichtsache und muss unter den Community-Mitgliedern vereinbart werden.
| PR-Kommentar | |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Was ist wenn Tätigkeit A in Community X vergütet wird und Tätigkeit B in Community Y:<br />- Ich übe Tätigkeiten A & B aus<br />- Muss ich mich entscheiden, welche Tätigkeit ich vergütet wissen will? (Stichwort eindeutige Registrierung im gesammten Netzwerk) |
| Claus-Peter 25.11.2021 | Gute Frage!<br />Ich denke wir sollten im 1. Schritt damit beginnen, dass jede Community ihre eigene Aktivitätenliste pflegt. Es werden dann wohl gleiche Aktivitäten in mehreren Listen der verschiedenen Communities auftauchen.<br /><br />Ich kann aber als Mitglied der Community A eine Tätigkeit A.X von einem Mitglied aus Community B bestätigt bekommen und natürlich auch von Mitgliedern aus meine Community A.<br /><br />Dazu müssen dann natürlich die notwendigen Informationen zw. den Communities für eine Cross-Community-Bestätigung ausgetauscht werden. Das führt dann genau zu dem gewünschten Effekt, dass zw. zwei Communities ein Informationsaustausch stattfindet, der dann eine Aussage über den Cross-Handel und das Vertrauen ermöglicht. |
Zu der Liste der Tätigkeiten gibt es einen weiteren Prozess, der in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) näher beschrieben ist. Hier kann soviel erst einmal gesagt werden, dass die Tätigkeitenliste als Grundlage dient, damit ein Mitglied für seine erbrachten Leistungen für das Allgemeinwohl dann sein monatliches *Aktives Grundeinkommen* gutgeschrieben bekommt. Dieses Gutschreiben des AGEs unterliegt noch einer vorherigen Bestätigung von anderen Community- oder auch Community übergreifenden Mitgliedern. Somit erfolgt dadurch eine implizite Vernetzung der Mitglieder durch dieses aktive Bestätigen anderer Leistungen, was gleichzeitig wieder Vorraussetzung ist, um sein eigenes AGE zu erhalten.
### Berechtigungsverwaltung
Die Community muss für die verschiedenen Eigenschaften und Prozesse eine eigene Berechtigungsverwaltung zur Verfügung stellen. Für die verschiedenen Berechtigungen muss ein Rollen- und Rechte-Konzept administrierbar sein, so dass für die verschiedenen Mitglieder der Community die Zugriffe feingranular definiert, gesteuert und kontrolliert werden können. Allein der Administrator hat die Rechte auf die Berechtigungsverwaltung zuzugreifen. Das System muss diese hinterlegten Rollen und Rechte dann auf die verwalteten Mitglieder abbilden und für jeden Zugriff auf die Community entsprechend kontrollieren, freigeben oder verhindern.
### Vernetzung und Vertrauensbildung
Mit der Vernetzung der Communities und dem gemeinsamen Handel zwischen Community-Mitgliedern innerhalb des gesamten Netzwerks entsteht automatisch ein Vertrauensverhältnis zwischen den verschiedenen Communities und auch zwischen den Community-Mitgliedern. Diese sich dynamisch verändernde Vertrauensverhältnisse können als Graph aufbereitet und zu weiteren Auswertungen bzw. Priorisierungen von fachlichen Prozessen herangezogen werden. Da in dem Gradido-Netzwerk der Mensch und das gegenseitige Vertrauen im Mittelpunkt steht, benötigt er für seine Bewertungen und Entscheidungen von Handel und Austausch mit anderen Communities bzw. anderen Mitgliedern ein Werkzeug, das ihm diese Informationen liefern kann. Das bedeutet in der Gradido-Anwendung werden statistische Werte über die Kommunikation zwischen Communities und zwischen Mitgliedern erhoben, die als Grundlage für den Vertrauensgraphen dienen.
### Attribute einer Community
In diesem Kapitel werden die Attribute beschrieben, die in einer Community zu speichern sind.
@ -166,27 +225,39 @@ Das Attribut *Parent* dient für den hierarchischen Aufbau von Communities. Es e
Das Attribut *Liste Children* dient ebenfalls dem hierarchischen Aufbau von Communities. Es enthält die Bezüge auf die Communities, die für diese Community als Child-Community eingerichtet sind. Eine Parent-Community kann mehrere Child-Communities haben. Durch diesen Bezug zu den Child-Communities werden einzelne Prozesse zwischen der Parent- und den Child-Communities freigeschalten. Damit ergeben sich erweiterte Möglichkeiten u.a. für die Community-Mitglieder beider Communities, wie beispielsweise das Community übergreifende Handeln zwischen den Community-Mitgliedern oder eine veränderte Verteiltung der Gemeinwohl- und AUF-Schöpfung, etc.. Die Administration dieses Attributes erfolgt implizit über die fachlichen Prozesse, die den Auf- und Abbau einer Parent-Child-Beziehung zwischen zwei Communities steuern. Diese können nur durch den Administrator und seiner Berechtigung ausgelöst werden. Die Beschreibung dieser Prozesse ist weiter unten im Kapitel **Anwendungsfälle auf einer Community** zu finden.
#### Liste Trusted Communities
Das Attribut *Liste Trusted Communities* dient dem Aufbau von gleichberechtigten Community-Gruppierungen. Es enthält die Referenzen auf die Communities, die für diese Community als vertrauenswürdige Communities eingerichtet sind. Eine vertrauenswürdige Community-Gruppierung kann mehrere gleichberechtigte Communities haben. Durch diesen Bezug zu den vertrauenswürdigen Communities werden einzelne Prozesse zwischen den sich gegenseitig vertrauenden Communities freigeschalten. Damit ergeben sich erweiterte Möglichkeiten u.a. für die Community-Mitglieder beider Communities, wie beispielsweise das Community übergreifende Handeln zwischen den Community-Mitgliedern, etc.. Zwischen zwei *Trusted Communities* erfolgt keine Verteilung gemäß einem Verteilungsschlüssel von geschöpftem Geld das für das Allgemeinwohl- bzw. AUF-Konto bestimmt ist. Dies bleibt Eigentum jeder Community trotz vertrauenswürdiger Beziehung untereinander.
Die Administration dieses Attributes erfolgt implizit über die fachlichen Prozesse, die den Auf- und Abbau einer vertrauenswürdigen Beziehung zwischen zwei Communities steuern. Diese können nur durch den Adminitrator und seiner Berechtigung ausgelöst werden. Die Beschreibung dieser Prozesse ist im nachfolgenden Kapitel **Anwendungsfälle auf einer Community** zu finden.
## Anwendungsfälle auf einer Community
Die nachfolgenden Anwendungsfälle beschreiben die fachlichen Vorraussetzungen, den fachlichen Ablauf und die fachlichen Veränderungen bzw. den fachlichen Status, der am Ende des erfolgreich abgeschlossenen Anwendungsfalles erreicht wird. Desweiteren erfolgt die fachliche Beschreibung der möglichen Fehlerfälle, in die ein Anwendungsfall münden kann und welcher fachlicher Status am Ende des Anwendungsfalles herrschen soll.
### Neue Community erstellen
*Allgemeine fachliche Beschreibung des Anwendungsfalles.*
Der Prozess *Neue Community erstellen* kann in zwei grundlegende Schritte untergliedert werden. Im ersten Schritt erfolgt der Aufbau und die Einrichtung der technischen Infrastruktur, die für den Betrieb der neuen Community benötigt wird. Im zweiten Schritt wird dann die eigentliche Inbetriebnahme der neuen Community durchgeführt, bei der die notwendigen Registrierungsschritte für den fachlichen Austausch an Informationen zwischen den schon bestehenden Communities und der neuen Community erfolgt. Der erste Schritt wird hier im Kapitel Vorraussetzungen beschrieben. Der zweite Schritt dieses Prozesse erfolgt als Ablaufbeschreibung der Registrierungsschritte im Kapitel Ablauf.
#### Vorraussetzungen
Um eine neue Community zu erstellen wird eine dafür speziell konzepierte Infrastruktur benötigt. Die technischen Details dieser Infrastruktur werden in der *technischen Infrastruktur Beschreibung* als eigenständiges Dokument dem Administrator der neuen Community zur Verfügung gestellt. Diese ist neben den Installationsskripten und Anwendungsdateien Teil des Auslieferungspaketes der Gradido-Anwendung.
Sobald der Administrator die geforderte Infrastruktur in Betrieb genommen und darauf die entsprechenden Installationsskripte ausgeführt hat erfolgt die eigentliche Erstellung und Registrierung der neue Community. Das heißt beim erstmaligen Start der Gradido-Anwendung wird automatisch der Prozess *Neue Community erstellen* gestartet.
#### Ablauf
Der Prozess *Neue Community erstellen* wird entweder automatisiert beim erstmaligen Start der Gradido-Anwendung auf einer neuen Infrastruktur gestartet oder manuell, wenn eine neue Community auf einer schon bestehenden Infrastruktur zusätzlich eingerichtet werden soll. Die nachfolgende Ablaufgrafik zeigt die logischen Schritte, die in dem Prozess durchlaufen werden:
![Ablauf Neue Community erstellen](./image/Ablauf_Neue_Community_erstellen.png)
#### Ende Status
1. Community-Infrastruktur ist installiert und aktiv
2. neue Community ist erzeugt und Daten in der Community-DB gespeichert
3. der Hintergrundprozess "Community-Vernetzung" ist am Laufen
* die initiale "newCommunity-Msg" mit den eigenen Community-Daten ist in den Public-Channel versendet
* ein Listener lauscht am Public-Channel auf Antworten (replyNewCommunityMsg) der schon existenten Communities
* ein Listener lauscht am Public-CHannel auf initiale "newCommunity-Msg" anderer neuer Communities
4. mit dem ersten Empfangen einer Reply-Msg einer anderen Community, wird der Community-Connection Prozess gestartet, der mit jedem Empfang von neuen Community-Daten eine P2P-Verbindung zu dieser Community aufbaut, um direkt detaillierte Daten auszutauschen
5. die vordefinierte Tätigkeitsliste ist geladen
6. die vordefinierten Berechtigungen sind aktiv
7. optional sind schon Mitglieder erfasst und in der Datenbank gespeichert
#### Fehlerfälle
### Community bearbeiten
@ -260,3 +331,78 @@ Die nachfolgenden Anwendungsfälle beschreiben die fachlichen Vorraussetzungen,
#### Ende Status
#### Fehlerfälle
# Besprechung 19.08.2021 19:00 mit Bernd
## Kreis-Mensch-Sein-Community
Felix Kramer
noch keine eigene Währung, wollen gerne Gradido
haben auch aktives Grundeinkommen
passt aber nicht ganz zur Gradido Philosophie, weil Gemeinwohlleistung zu unterschiedlich bewertet werden.
-> Colored Gradido?
Community-Creation
GDD1 (gold) ist existent
Felix baut GGD2-Infrastruktur auf
* Frage: willst du GDD1(gold) oder eigene Währung?
* Antwort: nein ich will eigene GDD2 (rot)
* muss neue Währung erzeugen
* Antwort: ja, dann Anfrage an GDD1, dass GDD2 auch Goldene GDD1 schöpfen darf?
* Ja wird akzeptiert
* dann bekommt GDD2 die Lizenz goldene GDD1 schöpfen kann
Kommt später heraus, dass GDD2 nicht mehr den goldenen Regeln entspricht, dann muss die Lizenz zum goldene GDD1 Schöpfen für GDD2 gesperrt werden.
Bisher geschöpfte goldene GDD2 beleiben aber erhalten.
Es darf keine Markierung des Bot-Mitglieds geben, da Missbrauch/Fehler möglich
Identität für ein Mitglied muss Human/Nichthuman enthalten
GDD2 muss mit Lizenzentzug wechseln auf eigene Währung um weiterschöpfen zu können.
Mitgliederwechsel in andere Community muss dann auch Währungswechsel berücksichtigen.
Bestcase: 1 Blockchain pro Währung
GDD1(gold) existent
GDD2(gold) soll gegründet werden
GDD2 baut Infrasturktur auf
Frage an GDD2, ob goldene oder andere?
### Tätigkeiten, die von der Community aktzeptiert werden
Nachweise für durchgeführte Tätigkeiten, bevor diese dem AGE-Konto gutgeschrieben werden?
Liste der Tätigkeiten muss von Community erstellt, bestätigt und verwaltet werden
Bei Tätigkeit von x Stunden für das AGE muss aus der Liste die passende Tätigkeit gewählt werden und per Nachweis (andere Mitglieder, Video, o.ä.)
Bei Krankheit o.ä. muss es aber möglich sein, dass dennoch Geld auf das AGE-Konto kommt.
| PR-Kommentar | |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Definition? Ist Faulheit eine Krankheit? |
| Claus-Peter 25.11.2021 | :-) Das kommt auf die solidarische Einstellung der Community an ;-)<br /><br />da drängt sich mir die Gegenfrage auf: Bis zu welchem Alter bekommt ein Kind sein AGE-Geld geschöpft nur durch seine blos Existenz? Oder andersherum, ab und bis zu welchem Alter muss eine Gegenleistung erbracht werden, Stichworte: unbeschwerte Kindheit und wohlverdiente Altersruhe? |
Kontaktförderung durch gewichtete Tätigkeitsbestätigung ( bei mind. 2 Bestätigungen pro Tätigkeit muss mind. ein neues Mitglied dabei sein)
Liste von Mitgliedern, die ich bestätigt habe:
* Kontaktpflege
* Gewichtung
* Vernetzung
Ricardo Leppe Podcast Lern und Memotechniken

View File

@ -1,3 +1,33 @@
# Regeln der Geldschöpfung
*Hier werden die fachlichen Regeln der 3-fachen Schöpfung beschrieben*
Nach der Gradido-Philosophie erfolgt pro Monat der Prozess der Dreifachen-Geldschöpfung. Das bedeutet, dass in einer Gradido-Community pro Mitglied, das eine *natürliche Person* ist, eine maximale Summe von 3.000 GDD geschöpft wird. Davon kann das Mitglied 1.000 GDD als *Aktives Grundeinkommen* durch Community nützige Leistungen auf sein persönliches AGE-Konto erhalten. 1.000 GDD werden auf das Gemeinwohlkonto der Community gutgeschrieben und weitere 1.000 GDD fließen auf das AUF-Konto der Community.
| PR-Kommentar | |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Hier wird immer ausgespart wie sich die 3x 1000 GDD zueinander verhalten. Als ich Bernd gefragt habe sagte er, das aus Planungssicherheit immer die vollen 2000 GDD für die Community geschöpft werden. Das halte ich allerdings für sehr gefährlich, da es dazu verleitet einfach weitere Konten zu registrieren. Hinter dem geschöpftem Geld steht dann auch ggf keine Leistung und damit keine Deckung. Ich halte es für essentiell die Schöpfung für AUF & Community GDD auch an die konkrete Schöpfung des Nutzers zu binden, da diese leistungsgedeckt ist. |
| Claus-Peter 25.11.2021 | Das ist eine berechtigte Frage und muss echt konzeptionell weiter durchdacht werden.<br />Ein Hinweis zu dem Thema wäre noch, dass egal ob und wieviel eine Person in dem Giralgeldsystem verdient, die Kommune, in der diese Person den 1.Wohnsitz hat, auch Steuergelder zugewiesen bekommt. |
Um diese Schritte des Geldschöpfungsprozesses zu gewährleisten bedarf es einiger Vorraussetzungen, die im folgenden beschrieben werden.
# Vorraussetzungen
## Drei Kontenmodell
## Verteilungsschlüssel
## Tätigkeitsliste für Aktives Grundeinkommen
Die Tätigkeitsliste für das AGE dient als Grundlage der gemeinnützigen Leistungen, die ein Mitglied sich anrechnen lassen kann. Das heißt eine Community muss eine Sammlung von Tätigkeiten erfassen, die von den Community-Mitgliedern als gemeinnützig empfunden und bestätigt werden.
Das heißt die Community muss unter den Mitgliedern eine Liste erarbeiten, die alle Tätigkeiten enthält, aus denen sich ein Mitglied dann eine oder mehrere auswählen kann, um sich sein Aktives Grundeinkommen damit zu decken. Die einzelnen Tätigkeiten sollen auch fachlich strukturierbar sein z.B. Kunst, Soziales, Gesundheit, Produktion, etc. . Die Menge und Definition der einzelnen Tätigkeiten und Strukturen unterliegt einer stetigen Anpassung nach den Bedürfnissen der Community-Mitglieder, um den natürlichen Veränderungen des miteinander Lebens gerecht werden zu können. Ob zu einer Tätigkeitsbeschreibung auch gleich eine Wertigkeit definiert werden soll, ist noch unklar. Man kann aber sicherlich sagen, dass manche Tätigkeiten dem Gemeinwohl dienlicher sind als andere. Aber auch das ist wiederum eine Ansichtsache und muss unter den Community-Mitgliedern vereinbart werden.
| PR-Kommentar | |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ulf 07.11.2021 | Ich denke die Liste sollte sich aus den Tätigkeiten der anerkannten Leistungen zusammensetzen, folglich vollautomatisch entstehen.<br />Die Zuordnung jeder Leistung zu einer Kategorie halte ich allerdings für sehr gut, da es einen Vergleich und eine Bewertung von Communities zulässt "Das ist eine Handwerker Community" "Das sind ja nur Baumschmuser" |
| Claus-Peter 25.11.2021 | Ich denke im ersten Schritt können wir aus den bisher aufgelaufenen Tätigkeiten während der Transformationsphase eine Art Default-Liste erzeugen, die dann von den Communities weiter ergänzt/verändert, etc. werden.<br />Ob wir hier evtl. schon Schrittweise in den jetzigen Adminbereich Mechanismen einbauen sollten, um eine Gliederung und feste Vorschlagstexte der Tätigkeitsbezeichnungen zu gewinnen, sollten wir im Hinterkopf behalten. |
### Erstellung und Erfassung der Tätigkeiten
# Prozess der Geldschöpfung

View File

@ -0,0 +1,304 @@
<mxfile>
<diagram id="Lc_Wy6ZhKx3Be9Prl_QG" name="Page-1">
<mxGraphModel dx="1088" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="28" value="Community-Prozess" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#82b366;strokeWidth=2;fillColor=#d5e8d4;gradientColor=#97d077;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="40" y="130" width="1220" height="910" as="geometry"/>
</mxCell>
<mxCell id="27" value="Prozess: Community-Vernetzung" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#6c8ebf;strokeWidth=2;fillColor=#dae8fc;gradientColor=#7ea6e0;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="610" y="264.5" width="640" height="555.5" as="geometry"/>
</mxCell>
<mxCell id="30" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontColor=#000000;strokeColor=#000000;" parent="1" source="2" target="4" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="185" y="170"/>
<mxPoint x="250" y="170"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="2" value="automatisch beim 1.Start der Anwendung" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;rounded=1;" parent="1" vertex="1">
<mxGeometry x="170" y="40" width="30" height="60" as="geometry"/>
</mxCell>
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="3" target="4" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="250" y="210" as="targetPoint"/>
<Array as="points">
<mxPoint x="405" y="170"/>
<mxPoint x="250" y="170"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="77" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="3" target="75">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="405" y="170"/>
<mxPoint x="450" y="170"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="95" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.75;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="3" target="38">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="560" y="70"/>
<mxPoint x="560" y="790"/>
<mxPoint x="485" y="790"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="manuell durch Administrator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;rounded=1;" parent="1" vertex="1">
<mxGeometry x="390" y="40" width="30" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="4" target="5" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="4" value="Initialisiere Prozess &lt;br&gt;&quot;Neue Community erstellen&quot;" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="150" y="202.63" width="200" height="40" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="5" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="5" value="Attribute der Community erfassen &lt;br&gt;bzw. aus Config lesen&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Name&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Icon / Bild (opt.)&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Beschreibung&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- hosted Server / URL&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Währungsname (opt.)&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Währungskürzel (opt.)&lt;/span&gt;&lt;/div&gt;" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="150" y="280" width="200" height="120" as="geometry"/>
</mxCell>
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#000000;exitX=0.75;exitY=0;exitDx=0;exitDy=0;" parent="1" source="24" target="13" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="520" y="420" as="targetPoint"/>
<Array as="points">
<mxPoint x="425" y="451"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="51" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#1A1A1A;" edge="1" parent="1" source="7" target="50">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="7" value="erzeuge techn. Community-Keys:&lt;br&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Community-ID&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: left&quot;&gt;&lt;span&gt;- Community-Currency-ID&lt;/span&gt;&lt;/div&gt;" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="150" y="423.13" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="17" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="13" target="16" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="70" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="13" target="72">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="785" y="364.5"/>
<mxPoint x="785" y="364.5"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="74" value="Nein" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#B0E3E6;rounded=1;" vertex="1" connectable="0" parent="70">
<mxGeometry x="-0.2906" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="13" value="Community-Attribute:&#10;(Name, Community-ID, URL)&#10;vorhanden?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" parent="1" vertex="1">
<mxGeometry x="690" y="410.5" width="190" height="80" as="geometry"/>
</mxCell>
<mxCell id="19" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="16" target="20" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="785" y="517.13" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="16" value="Sende Community-Attribute&lt;br&gt;auf public Channel" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="960" y="429.76" width="210" height="40" as="geometry"/>
</mxCell>
<mxCell id="83" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="20" target="80">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="20" value="lausche an public Channel&lt;br&gt;auf&lt;br&gt;&quot;replyNewCommuntiy-Msg&quot;&lt;br&gt;&quot;newCommuntiy-Msg&quot;" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="960" y="511.62999999999994" width="210" height="65" as="geometry"/>
</mxCell>
<mxCell id="25" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="22" target="20" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="910" y="544"/>
<mxPoint x="910" y="544"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="22" value="speichere empfangene&lt;br&gt;Community-Daten in&lt;br&gt;Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="690" y="516.6299999999999" width="210" height="55" as="geometry"/>
</mxCell>
<mxCell id="24" value="Starte Community-Vernetzung &lt;br&gt;als Hintergrundprozess" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="290" y="622.63" width="180" height="39.5" as="geometry"/>
</mxCell>
<mxCell id="37" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="34" target="36" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="34" value="Lade &lt;br&gt;Default-Tätigkeitsliste,&lt;br&gt;Standard-Berechtigungen,&lt;br&gt;etc." style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="130" y="607.38" width="140" height="70" as="geometry"/>
</mxCell>
<mxCell id="46" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="36" target="39" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="47" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#000000;rounded=1;labelBackgroundColor=#97D077;" parent="46" vertex="1" connectable="0">
<mxGeometry x="0.24" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="48" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="36" target="43" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="49" value="Nein" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#000000;rounded=1;labelBackgroundColor=#97D077;" parent="48" vertex="1" connectable="0">
<mxGeometry x="0.2056" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="36" value="neue Mitglieder&lt;br&gt;erfassen?" style="rhombus;whiteSpace=wrap;html=1;fontColor=#ffffff;strokeColor=#2D7600;strokeWidth=2;fillColor=#60a917;rounded=1;" parent="1" vertex="1">
<mxGeometry x="130" y="754.5" width="140" height="80" as="geometry"/>
</mxCell>
<mxCell id="38" value="Community-Prozess" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#82b366;strokeWidth=2;fillColor=#d5e8d4;gradientColor=#97d077;fontColor=#000000;rounded=1;" parent="1" vertex="1">
<mxGeometry x="290" y="834.5" width="260" height="120" as="geometry"/>
</mxCell>
<mxCell id="45" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontColor=#FFFFFF;strokeColor=#000000;" parent="1" source="39" target="43" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="510" y="990"/>
<mxPoint x="200" y="990"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="39" value="Prozess &lt;br&gt;&quot;Neuen Benutzer anlegen&quot;" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
<mxGeometry x="330" y="874.5" width="200" height="40" as="geometry"/>
</mxCell>
<mxCell id="43" value="" style="ellipse;html=1;shape=endState;fillColor=#000000;strokeColor=#000000;labelBackgroundColor=#97D077;fontColor=#FFFFFF;rounded=1;" parent="1" vertex="1">
<mxGeometry x="185" y="1060" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="53" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0.514;entryDx=0;entryDy=0;entryPerimeter=0;strokeColor=#1A1A1A;" edge="1" parent="1" source="50" target="52">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="50" value="speichere Community-Daten &lt;br&gt;in Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
<mxGeometry x="150" y="503.13" width="200" height="39.5" as="geometry"/>
</mxCell>
<mxCell id="54" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#1A1A1A;exitX=0.374;exitY=0.232;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="52" target="24">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="302" y="596"/>
<mxPoint x="380" y="596"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="55" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#1A1A1A;exitX=-0.226;exitY=0.784;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="52" target="34">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="52" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;fillColor=#000000;strokeColor=none;rotation=90;rounded=1;" vertex="1" parent="1">
<mxGeometry x="250" y="480.13" width="5" height="185" as="geometry"/>
</mxCell>
<mxCell id="62" value="newCommunity-Msg" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=0;notch2=20;align=center;verticalAlign=middle;fontSize=14;fontStyle=0;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=15;" vertex="1" parent="1">
<mxGeometry x="1184.99" y="463.13" width="170" height="20" as="geometry"/>
</mxCell>
<mxCell id="69" value="&lt;font style=&quot;font-size: 12px&quot;&gt;replyNewCommunity-Msg&lt;/font&gt;" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=20;notch2=0;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;flipH=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="1180.01" y="519.51" width="160" height="20" as="geometry"/>
</mxCell>
<mxCell id="73" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="72" target="5">
<mxGeometry relative="1" as="geometry">
<Array as="points"/>
</mxGeometry>
</mxCell>
<mxCell id="72" value="Fehlermeldung &lt;br&gt;wegen&lt;br&gt;fehlender Parameter" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;strokeColor=#2D7600;fillColor=#B0E3E6;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="725" y="314.5" width="120" height="50" as="geometry"/>
</mxCell>
<mxCell id="76" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="75" target="13">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="450" y="451"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="75" value="Starte Community-Vernetzung &lt;br&gt;als Hintergrundprozess" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
<mxGeometry x="360" y="202.63" width="180" height="39.5" as="geometry"/>
</mxCell>
<mxCell id="79" value="&lt;font style=&quot;font-size: 12px&quot;&gt;newCommunity-Msg&lt;/font&gt;" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=20;notch2=0;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;flipH=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=0;" vertex="1" parent="1">
<mxGeometry x="1180.01" y="546.39" width="160" height="20" as="geometry"/>
</mxCell>
<mxCell id="81" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="80" target="22">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#7EA6E0;" vertex="1" connectable="0" parent="81">
<mxGeometry x="-0.4267" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="85" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="80" target="84">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="80" value="replyNewCommunityMsg?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
<mxGeometry x="970" y="588.76" width="190" height="47.87" as="geometry"/>
</mxCell>
<mxCell id="93" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0.499;exitY=0.966;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="84" target="86">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1065" y="717.8699999999999" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="96" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#7EA6E0;" vertex="1" connectable="0" parent="93">
<mxGeometry x="-0.2741" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="84" value="NewCommunityMsg?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
<mxGeometry x="970" y="650" width="190" height="47.87" as="geometry"/>
</mxCell>
<mxCell id="94" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="86" target="22">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="86" value="Sende Community-Attribute&lt;br&gt;auf public Channel" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="960" y="730" width="210" height="40" as="geometry"/>
</mxCell>
<mxCell id="89" value="replyNewCommunity-Msg" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=0;notch2=20;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=-45;" vertex="1" parent="1">
<mxGeometry x="1179.98" y="657.38" width="180.01" height="20" as="geometry"/>
</mxCell>
<mxCell id="97" value="" style="group" vertex="1" connectable="0" parent="1">
<mxGeometry x="1359.9950000000001" y="460.0049999999999" width="232.6199999999999" height="182.6300000000001" as="geometry"/>
</mxCell>
<mxCell id="65" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;align=center;rotation=-90;strokeColor=#36393d;fillColor=#66B2FF;rounded=1;" vertex="1" parent="97">
<mxGeometry x="24.99499999999989" y="-24.99499999999989" width="182.63" height="232.62" as="geometry"/>
</mxCell>
<mxCell id="66" value="public Channel" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontColor=#000000;fontSize=14;rounded=1;" vertex="1" parent="97">
<mxGeometry x="96.30500000000006" y="74.99500000000012" width="40" height="20" as="geometry"/>
</mxCell>
<mxCell id="98" value="Prozess: Community-Connection" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#6c8ebf;strokeWidth=2;fillColor=#dae8fc;gradientColor=#7ea6e0;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="610" y="834.5" width="640" height="185.5" as="geometry"/>
</mxCell>
<mxCell id="106" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="99" target="105">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="99" value="stelle P2P-Verbindung zu &lt;br&gt;neuer Community her&lt;br&gt;und tausche detailiete Daten aus" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="974.99" y="881" width="210" height="67" as="geometry"/>
</mxCell>
<mxCell id="102" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="100" target="99">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="100" value="Daten einer neuen&#10;Community erhalten?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
<mxGeometry x="637" y="874.5" width="190" height="80" as="geometry"/>
</mxCell>
<mxCell id="101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="22" target="100">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="670" y="544"/>
<mxPoint x="670" y="790"/>
<mxPoint x="732" y="790"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="103" value="" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;" edge="1" parent="1">
<mxGeometry width="100" height="100" relative="1" as="geometry">
<mxPoint x="1190.01" y="914" as="sourcePoint"/>
<mxPoint x="1340.01" y="914" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="104" value="neue Community" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fontSize=14;fontColor=#000000;fillColor=#B0E3E6;align=center;gradientColor=#97D077;" vertex="1" parent="1">
<mxGeometry x="1359.99" y="840" width="240.01" height="160" as="geometry"/>
</mxCell>
<mxCell id="107" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="105" target="100">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="speichere empfangene&lt;br&gt;Community-Daten in&lt;br&gt;Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
<mxGeometry x="810" y="954.5" width="150" height="55" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

View File

@ -0,0 +1,100 @@
<mxfile host="65bd71144e">
<diagram id="5Rbgv0eSL3tDJ_YiGKbA" name="Page-1">
<mxGraphModel dx="1406" dy="633" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Release 1.6.X" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="40" width="560" height="80" as="geometry"/>
</mxCell>
<mxCell id="3" value="Development&lt;br style=&quot;font-size: 12px&quot;&gt;15.10.2021 -&lt;br style=&quot;font-size: 12px&quot;&gt;30.12.2021&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="120" width="400" height="80" as="geometry"/>
</mxCell>
<mxCell id="4" value="Testing&lt;br style=&quot;font-size: 7px;&quot;&gt;31.12.2021 -&lt;br style=&quot;font-size: 7px;&quot;&gt;07.01.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="440" y="120" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="5" value="Release Window&lt;br style=&quot;font-size: 7px;&quot;&gt;08.01.2022 -&lt;br style=&quot;font-size: 7px;&quot;&gt;15.01.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="480" y="120" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="6" value="Deadline&lt;br style=&quot;font-size: 12px;&quot;&gt;31.01.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="520" y="120" width="80" height="80" as="geometry"/>
</mxCell>
<mxCell id="7" value="Release 1.7.X" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="600" y="40" width="560" height="80" as="geometry"/>
</mxCell>
<mxCell id="8" value="Admin Interface" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="200" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="9" value="Replace Login Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="240" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="10" value="Replace Community Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="280" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="11" value="Refine/Partially redesign Register Process" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="320" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="12" value="PublisherID Field on Register" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="360" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="13" value="GDD Decay Calculator Tool" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="400" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="14" value="Refactors, Tests &amp;amp; Devops (e.g. Seeding)" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="560" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="15" value="Seperate Overview from SendCoin" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="440" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="16" value="Minor Design &amp;amp; Layout changes in Frontend" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="480" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="17" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="440" y="200" width="40" height="400" as="geometry"/>
</mxCell>
<mxCell id="18" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="480" y="200" width="40" height="400" as="geometry"/>
</mxCell>
<mxCell id="19" value="New Deployment" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="520" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="22" value="Start refactoring" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="520" y="200" width="80" height="400" as="geometry"/>
</mxCell>
<mxCell id="23" value="Development&lt;br style=&quot;font-size: 12px&quot;&gt;01.02.2022 -&lt;br style=&quot;font-size: 12px&quot;&gt;28.02.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="120" width="400" height="80" as="geometry"/>
</mxCell>
<mxCell id="24" value="Testing&lt;br style=&quot;font-size: 12px&quot;&gt;01.03.2022 -&lt;br style=&quot;font-size: 12px&quot;&gt;07.03.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1000" y="120" width="80" height="80" as="geometry"/>
</mxCell>
<mxCell id="25" value="Release Window&lt;br style=&quot;font-size: 12px&quot;&gt;08.03.2022 -&lt;br style=&quot;font-size: 12px&quot;&gt;15.03.2022&amp;nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1080" y="120" width="80" height="80" as="geometry"/>
</mxCell>
<mxCell id="26" value="Refactoring" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="200" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="28" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="1000" y="200" width="80" height="240" as="geometry"/>
</mxCell>
<mxCell id="29" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
<mxGeometry x="1080" y="200" width="80" height="240" as="geometry"/>
</mxCell>
<mxCell id="30" value="Admin Interface User Tools" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="240" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="31" value="Make Klicktipp Community Ready" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="280" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="32" value="Make GDT Community Ready" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="320" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="33" value="Implement a Federation Solution &amp;amp; Experiment" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="360" width="400" height="40" as="geometry"/>
</mxCell>
<mxCell id="34" value="Frontend Community/Account statistics" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="600" y="400" width="400" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

BIN
docu/graphics/roadmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

@ -57,6 +57,7 @@
"at": "am",
"cancel": "Abbrechen",
"close": "schließen",
"current_balance": "aktueller Kontostand",
"date": "Datum",
"description": "Beschreibung",
"edit": "bearbeiten",
@ -66,6 +67,7 @@
"lastname": "Nachname",
"memo": "Nachricht",
"message": "Nachricht",
"new_balance": "neuer Kontostand nach Bestätigung",
"password": "Passwort",
"passwordRepeat": "Passwort wiederholen",
"password_new": "neues Passwort",
@ -90,7 +92,8 @@
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
"usernmae-regex": "Der Username muss mit einem Buchstaben beginnen auf den mindestens zwei alfanumerische Zeichen folgen müssen.",
"usernmae-unique": "Der Username ist bereits vergeben."
}
},
"your_amount": "Dein Betrag"
},
"gdt": {
"action": "Aktion",

View File

@ -57,6 +57,7 @@
"at": "at",
"cancel": "Cancel",
"close": "Close",
"current_balance": "current balance",
"date": "Date",
"description": "Description",
"edit": "Edit",
@ -66,6 +67,7 @@
"lastname": "Lastname",
"memo": "Message",
"message": "Message",
"new_balance": "account balance after confirmation",
"password": "Password",
"passwordRepeat": "Repeat password",
"password_new": "New password",
@ -90,7 +92,8 @@
"is-not": "You cannot send Gradidos to yourself",
"usernmae-regex": "The username must start with a letter, followed by at least two alphanumeric characters.",
"usernmae-unique": "The username is already taken."
}
},
"your_amount": "Your amount"
},
"gdt": {
"action": "Action",

View File

@ -251,6 +251,44 @@ describe('Login', () => {
it('toasts an error message', () => {
expect(toastErrorMock).toBeCalledWith('error.no-account')
})
describe('login fails with "User email not validated"', () => {
beforeEach(async () => {
apolloQueryMock.mockRejectedValue({
message: 'User email not validated',
})
wrapper = Wrapper()
jest.clearAllMocks()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('redirects to /thx/login', () => {
expect(mockRouterPush).toBeCalledWith('/thx/login')
})
})
describe('login fails with "User has no password set yet"', () => {
beforeEach(async () => {
apolloQueryMock.mockRejectedValue({
message: 'User has no password set yet',
})
wrapper = Wrapper()
jest.clearAllMocks()
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
await flushPromises()
await wrapper.find('form').trigger('submit')
await flushPromises()
})
it('redirects to /reset/login', () => {
expect(mockRouterPush).toBeCalledWith('/reset/login')
})
})
})
})
})

View File

@ -105,10 +105,10 @@ export default {
loader.hide()
})
.catch((error) => {
if (error.message.includes('No user with this credentials')) {
this.$toasted.global.error(this.$t('error.no-account'))
} else {
// : this.$t('error.no-email-verify')
this.$toasted.global.error(this.$t('error.no-account'))
if (error.message.includes('User email not validated')) {
this.$router.push('/thx/login')
} else if (error.message.includes('User has no password set yet')) {
this.$router.push('/reset/login')
}
loader.hide()

View File

@ -13,7 +13,9 @@ describe('SendOverview', () => {
const propsData = {
balance: 123.45,
transactionCount: 1,
GdtBalance: 1234.56,
transactions: [{ balance: 0.1 }],
pending: true,
}
const mocks = {
@ -42,18 +44,16 @@ describe('SendOverview', () => {
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
})
// it('has a transactions table', () => {
// expect(wrapper.find('div.gdd-transaction-list').exists()).toBeTruthy()
// })
describe('transaction form', () => {
it('steps forward in the dialog', async () => {
await wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
beforeEach(async () => {
wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
})
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBeTruthy()
})
it('steps forward in the dialog', () => {
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBe(true)
})
})
@ -112,18 +112,22 @@ describe('SendOverview', () => {
describe('transaction is confirmed and server response is error', () => {
beforeEach(async () => {
jest.clearAllMocks()
sendMock.mockRejectedValue({ message: 'receiver not found' })
sendMock.mockRejectedValue({ message: 'recipiant not known' })
await wrapper
.findComponent({ name: 'TransactionConfirmation' })
.vm.$emit('send-transaction')
})
it('shows the error page', () => {
expect(wrapper.find('div.card-body').text()).toContain('form.send_transaction_error')
expect(wrapper.find('.test-send_transaction_error').text()).toContain(
'form.send_transaction_error',
)
})
it('shows recipient not found', () => {
expect(wrapper.text()).toContain('transaction.receiverNotFound')
expect(wrapper.find('.test-receiver-not-found').text()).toContain(
'transaction.receiverNotFound',
)
})
})
})

View File

@ -1,12 +1,14 @@
<template>
<div>
<b-container>
<gdd-send :currentTransactionStep="currentTransactionStep">
<gdd-send :currentTransactionStep="currentTransactionStep" class="pt-3">
<template #transaction-form>
<transaction-form :balance="balance" @set-transaction="setTransaction"></transaction-form>
</template>
<template #transaction-confirmation>
<transaction-confirmation
:balance="balance"
:transactions="transactions"
:email="transactionData.email"
:amount="transactionData.amount"
:memo="transactionData.memo"
@ -44,7 +46,6 @@ export default {
name: 'SendOverview',
components: {
GddSend,
TransactionForm,
TransactionConfirmation,
TransactionResult,

View File

@ -2,23 +2,61 @@
<div>
<b-row>
<b-col>
<div class="display-4 p-4">{{ $t('form.send_check') }}</div>
<b-list-group>
<b-list-group-item class="d-flex justify-content-between align-items-center">
{{ email }}
<b-badge variant="primary" pill>{{ $t('form.recipient') }}</b-badge>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
{{ $n(amount, 'decimal') }} GDD
<b-badge variant="primary" pill>{{ $t('form.amount') }}</b-badge>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
{{ memo ? memo : '-' }}
<b-badge variant="primary" pill>{{ $t('form.message') }}</b-badge>
</b-list-group-item>
<div class="display-4 pb-4">{{ $t('form.send_check') }}</div>
<b-list-group class="">
<label class="input-1" for="input-1">{{ $t('form.recipient') }}</label>
<b-input-group id="input-group-1" class="borderbottom" size="lg">
<b-input-group-prepend class="d-none d-md-block gray-background">
<b-icon icon="envelope" class="display-4 m-3"></b-icon>
</b-input-group-prepend>
<div class="p-3">{{ email }}</div>
</b-input-group>
<br />
<label class="input-2" for="input-2">{{ $t('form.amount') }}</label>
<b-input-group id="input-group-2" class="borderbottom" size="lg">
<b-input-group-prepend class="p-2 d-none d-md-block gray-background">
<div class="m-1 mt-2">GDD</div>
</b-input-group-prepend>
<div class="p-3">{{ $n(amount, 'decimal') }}</div>
</b-input-group>
<br />
<label class="input-3" for="input-3">{{ $t('form.message') }}</label>
<b-input-group id="input-group-3" class="borderbottom">
<b-input-group-prepend class="d-none d-md-block gray-background">
<b-icon icon="chat-right-text" class="display-4 m-3 mt-4"></b-icon>
</b-input-group-prepend>
<div class="p-3">{{ memo ? memo : '-' }}</div>
</b-input-group>
</b-list-group>
</b-col>
</b-row>
<b-container class="bv-example-row mt-3 gray-background p-2">
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
<b-col class="text-right">{{ $n(balance, 'decimal') }}</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">
<strong>{{ $t('form.your_amount') }}</strong>
</b-col>
<b-col class="text-right">
<strong>- {{ $n(amount, 'decimal') }}</strong>
</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('decay.decay') }}</b-col>
<b-col class="text-right" style="border-bottom: double">- {{ $n(decay, 'decimal') }}</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
<b-col class="text-right">~ {{ $n(balance - amount - decay, 'decimal') }}</b-col>
</b-row>
</b-container>
<b-row class="mt-4">
<b-col>
<b-button @click="$emit('on-reset')">{{ $t('form.cancel') }}</b-button>
@ -35,10 +73,27 @@
export default {
name: 'TransactionConfirmation',
props: {
balance: { type: Number, default: 0 },
email: { type: String, default: '' },
amount: { type: Number, default: 0 },
memo: { type: String, default: '' },
loading: { type: Boolean, default: false },
transactions: {
default: () => [],
},
},
data() {
return {
decay: this.transactions[0].balance,
}
},
}
</script>
<style>
.gray-background {
background-color: #ecebe6a3 !important;
}
.borderbottom {
border-bottom: 1px solid rgb(70, 65, 65);
}
</style>

View File

@ -20,10 +20,10 @@
<div>{{ $t('form.sorry') }}</div>
<hr />
<div>{{ $t('form.send_transaction_error') }}</div>
<div class="test-send_transaction_error">{{ $t('form.send_transaction_error') }}</div>
<hr />
<div v-if="errorResult === 'receiver not found'">
<div class="test-receiver-not-found" v-if="errorResult === 'recipiant not known'">
{{ $t('transaction.receiverNotFound') }}
</div>
<div v-else>({{ errorResult }})</div>