mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge remote-tracking branch 'origin/master' into 3287-feature-gms-user-update-after-change-user-settings
This commit is contained in:
commit
89f0e9a694
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,8 +15,5 @@ package-lock.json
|
|||||||
/deployment/bare_metal/log
|
/deployment/bare_metal/log
|
||||||
/deployment/bare_metal/backup
|
/deployment/bare_metal/backup
|
||||||
|
|
||||||
# Node Version Manager configuration file
|
|
||||||
.nvmrc
|
|
||||||
|
|
||||||
# Apple macOS folder attribute file
|
# Apple macOS folder attribute file
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
1
admin/.nvmrc
Normal file
1
admin/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
v14.17.0
|
||||||
@ -9,7 +9,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 96,
|
lines: 95,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moduleFileExtensions: [
|
moduleFileExtensions: [
|
||||||
@ -31,6 +31,9 @@ module.exports = {
|
|||||||
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
setupFiles: ['<rootDir>/test/testSetup.js', 'jest-canvas-mock'],
|
||||||
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
testMatch: ['**/?(*.)+(spec|test).js?(x)'],
|
||||||
// snapshotSerializers: ['jest-serializer-vue'],
|
// snapshotSerializers: ['jest-serializer-vue'],
|
||||||
transformIgnorePatterns: ['<rootDir>/node_modules/(?!vee-validate/dist/rules)'],
|
transformIgnorePatterns: [
|
||||||
|
'<rootDir>/node_modules/(?!vee-validate/dist/rules)',
|
||||||
|
'/node_modules/(?!@babel)',
|
||||||
|
],
|
||||||
testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen
|
testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,13 @@
|
|||||||
"start": "node run/server.js",
|
"start": "node run/server.js",
|
||||||
"serve": "vue-cli-service serve --open",
|
"serve": "vue-cli-service serve --open",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
|
"postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +",
|
||||||
"dev": "yarn run serve",
|
"dev": "yarn run serve",
|
||||||
"analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
|
"analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
|
||||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||||
"test": "cross-env TZ=UTC jest",
|
"test": "cross-env TZ=UTC jest",
|
||||||
|
"test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit --no-cache --watch --runInBand",
|
||||||
"locales": "scripts/sort.sh"
|
"locales": "scripts/sort.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="federation-visualize-item">
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
|
||||||
<b-col>
|
|
||||||
<div>{{ item.url }}</div>
|
|
||||||
<small>{{ `${item.publicKey.substring(0, 26)}…` }}</small>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
|
|
||||||
<b-col cols="2">{{ createdAt }}</b-col>
|
|
||||||
</b-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
|
||||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
|
||||||
|
|
||||||
const locales = { en, de, es, fr, nl }
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'FederationVisualizeItem',
|
|
||||||
props: {
|
|
||||||
item: { type: Object },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
formatDistanceToNow,
|
|
||||||
locale: this.$i18n.locale,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
verified() {
|
|
||||||
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
|
|
||||||
},
|
|
||||||
icon() {
|
|
||||||
return this.verified ? 'check' : 'x-circle'
|
|
||||||
},
|
|
||||||
variant() {
|
|
||||||
return this.verified ? 'success' : 'danger'
|
|
||||||
},
|
|
||||||
lastAnnouncedAt() {
|
|
||||||
if (this.item.lastAnnouncedAt) {
|
|
||||||
return formatDistanceToNow(new Date(this.item.lastAnnouncedAt), {
|
|
||||||
includeSecond: true,
|
|
||||||
addSuffix: true,
|
|
||||||
locale: locales[this.locale],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
createdAt() {
|
|
||||||
if (this.item.createdAt) {
|
|
||||||
return formatDistanceToNow(new Date(this.item.createdAt), {
|
|
||||||
includeSecond: true,
|
|
||||||
addSuffix: true,
|
|
||||||
locale: locales[this.locale],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,36 +1,83 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
import Vuex from 'vuex'
|
||||||
|
import CommunityVisualizeItem from './CommunityVisualizeItem.vue'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
localVue.use(Vuex)
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const createdDate = new Date()
|
const createdDate = new Date()
|
||||||
createdDate.setDate(createdDate.getDate() - 3)
|
createdDate.setDate(createdDate.getDate() - 3)
|
||||||
|
|
||||||
|
// Mock für den Vuex-Store
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
state: {
|
||||||
|
moderator: {
|
||||||
|
roles: ['ADMIN'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
let propsData = {
|
let propsData = {
|
||||||
item: {
|
item: {
|
||||||
id: 7590,
|
id: 1,
|
||||||
foreign: false,
|
foreign: false,
|
||||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
url: 'http://localhost/api/',
|
||||||
url: 'http://localhost/api/2_0',
|
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
|
||||||
lastAnnouncedAt: createdDate,
|
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||||
verifiedAt: today,
|
authenticatedAt: null,
|
||||||
lastErrorAt: null,
|
name: 'Gradido Test',
|
||||||
|
description: 'Gradido Community zum testen',
|
||||||
|
gmsApiKey: '<api key>',
|
||||||
|
creationDate: createdDate,
|
||||||
createdAt: createdDate,
|
createdAt: createdDate,
|
||||||
updatedAt: null,
|
updatedAt: createdDate,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 2046,
|
||||||
|
apiVersion: '2_0',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
lastAnnouncedAt: createdDate,
|
||||||
|
verifiedAt: today,
|
||||||
|
lastErrorAt: null,
|
||||||
|
createdAt: createdDate,
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2045,
|
||||||
|
apiVersion: '1_1',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
createdAt: '2024-01-16T10:08:21.550Z',
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2044,
|
||||||
|
apiVersion: '1_0',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
createdAt: '2024-01-16T10:08:21.544Z',
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
|
$t: (key) => key,
|
||||||
$i18n: {
|
$i18n: {
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('FederationVisualizeItem', () => {
|
describe('CommunityVisualizeItem', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(FederationVisualizeItem, { localVue, mocks, propsData })
|
return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
@ -39,19 +86,35 @@ describe('FederationVisualizeItem', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the component', () => {
|
it('renders the component', () => {
|
||||||
expect(wrapper.find('div.federation-visualize-item').exists()).toBe(true)
|
expect(wrapper.exists()).toBe(true)
|
||||||
|
expect(wrapper.find('div.community-visualize-item').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.details').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles details on click', async () => {
|
||||||
|
// Click the row to toggle details
|
||||||
|
await wrapper.find('.row').trigger('click')
|
||||||
|
|
||||||
|
// Assert that details are now open
|
||||||
|
expect(wrapper.find('.details').exists()).toBe(true)
|
||||||
|
|
||||||
|
// Click the row again to toggle details back
|
||||||
|
await wrapper.find('.row').trigger('click')
|
||||||
|
|
||||||
|
// Assert that details are now closed
|
||||||
|
expect(wrapper.find('.details').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('rendering item properties', () => {
|
describe('rendering item properties', () => {
|
||||||
it('has the url', () => {
|
it('has the url', () => {
|
||||||
expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe(
|
expect(wrapper.find('.row > div:nth-child(2) > div > a').text()).toBe(
|
||||||
'http://localhost/api/2_0',
|
'http://localhost/api/',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has the public key', () => {
|
it('has the public key', () => {
|
||||||
expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain(
|
expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain(
|
||||||
'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7'.substring(0, 26),
|
'4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2'.substring(0, 26),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -65,33 +128,6 @@ describe('FederationVisualizeItem', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('not verified item', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
propsData = {
|
|
||||||
item: {
|
|
||||||
id: 7590,
|
|
||||||
foreign: false,
|
|
||||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
|
||||||
url: 'http://localhost/api/2_0',
|
|
||||||
lastAnnouncedAt: createdDate,
|
|
||||||
verifiedAt: null,
|
|
||||||
lastErrorAt: null,
|
|
||||||
createdAt: createdDate,
|
|
||||||
updatedAt: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
wrapper = Wrapper()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has the x-circle icon', () => {
|
|
||||||
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has the text variant "danger"', () => {
|
|
||||||
expect(wrapper.find('.text-danger').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// describe('with different locales (de, en, fr, es, nl)', () => {
|
// describe('with different locales (de, en, fr, es, nl)', () => {
|
||||||
describe('lastAnnouncedAt', () => {
|
describe('lastAnnouncedAt', () => {
|
||||||
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
|
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
|
||||||
@ -155,6 +191,30 @@ describe('FederationVisualizeItem', () => {
|
|||||||
expect(wrapper.vm.createdAt).toBe('3 dagen geleden')
|
expect(wrapper.vm.createdAt).toBe('3 dagen geleden')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('not verified item', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
propsData = {
|
||||||
|
item: {
|
||||||
|
id: 7590,
|
||||||
|
foreign: false,
|
||||||
|
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||||
|
url: 'http://localhost/api/',
|
||||||
|
createdAt: createdDate,
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has the x-circle icon', () => {
|
||||||
|
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has the text variant "danger"', () => {
|
||||||
|
expect(wrapper.find('.text-danger').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('createdAt == null', () => {
|
describe('createdAt == null', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {
|
propsData = {
|
||||||
@ -163,9 +223,9 @@ describe('FederationVisualizeItem', () => {
|
|||||||
foreign: false,
|
foreign: false,
|
||||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||||
url: 'http://localhost/api/2_0',
|
url: 'http://localhost/api/2_0',
|
||||||
lastAnnouncedAt: createdDate,
|
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||||
verifiedAt: null,
|
authenticatedAt: null,
|
||||||
lastErrorAt: null,
|
creationDate: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
},
|
},
|
||||||
160
admin/src/components/Federation/CommunityVisualizeItem.vue
Normal file
160
admin/src/components/Federation/CommunityVisualizeItem.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<div class="community-visualize-item">
|
||||||
|
<b-row @click="toggleDetails">
|
||||||
|
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
||||||
|
<b-col>
|
||||||
|
<div>
|
||||||
|
<a :href="item.url" target="_blank">{{ item.url }}</a>
|
||||||
|
</div>
|
||||||
|
<small>{{ `${item.publicKey.substring(0, 26)}…` }}</small>
|
||||||
|
</b-col>
|
||||||
|
<b-col v-b-tooltip="item.description">{{ item.name }}</b-col>
|
||||||
|
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
|
||||||
|
<b-col cols="2">{{ createdAt }}</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row v-if="details" class="details">
|
||||||
|
<b-col colspan="5">
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item v-if="item.uuid">
|
||||||
|
{{ $t('federation.communityUuid') }} {{ item.uuid }}
|
||||||
|
</b-list-group-item>
|
||||||
|
<b-list-group-item v-if="item.authenticatedAt">
|
||||||
|
{{ $t('federation.authenticatedAt') }} {{ item.authenticatedAt }}
|
||||||
|
</b-list-group-item>
|
||||||
|
<b-list-group-item>
|
||||||
|
{{ $t('federation.publicKey') }} {{ item.publicKey }}
|
||||||
|
</b-list-group-item>
|
||||||
|
<b-list-group-item v-if="!item.foreign">
|
||||||
|
{{ $t('federation.gmsApiKey') }}
|
||||||
|
<editable-label
|
||||||
|
:value="gmsApiKey"
|
||||||
|
:allowEdit="$store.state.moderator.roles.includes('ADMIN')"
|
||||||
|
@save="handleSaveGsmApiKey"
|
||||||
|
/>
|
||||||
|
</b-list-group-item>
|
||||||
|
<b-list-group-item>
|
||||||
|
<b-list-group>
|
||||||
|
<b-row>
|
||||||
|
<b-col class="ml-1">{{ $t('federation.verified') }}</b-col>
|
||||||
|
<b-col>{{ $t('federation.apiVersion') }}</b-col>
|
||||||
|
<b-col>{{ $t('federation.createdAt') }}</b-col>
|
||||||
|
<b-col>{{ $t('federation.lastAnnouncedAt') }}</b-col>
|
||||||
|
<b-col>{{ $t('federation.verifiedAt') }}</b-col>
|
||||||
|
<b-col>{{ $t('federation.lastErrorAt') }}</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="federation in item.federatedCommunities"
|
||||||
|
:key="federation.id"
|
||||||
|
:variant="!item.foreign ? 'primary' : 'warning'"
|
||||||
|
>
|
||||||
|
<federation-visualize-item :item="federation" />
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||||
|
import EditableLabel from '@/components/input/EditableLabel'
|
||||||
|
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
||||||
|
import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'
|
||||||
|
|
||||||
|
const locales = { en, de, es, fr, nl }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommunityVisualizeItem',
|
||||||
|
components: {
|
||||||
|
EditableLabel,
|
||||||
|
FederationVisualizeItem,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: { type: Object },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formatDistanceToNow,
|
||||||
|
locale: this.$i18n.locale,
|
||||||
|
details: false,
|
||||||
|
gmsApiKey: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.gmsApiKey = this.item.gmsApiKey
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
verified() {
|
||||||
|
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
this.item.federatedCommunities.filter(
|
||||||
|
(federatedCommunity) =>
|
||||||
|
new Date(federatedCommunity.verifiedAt) >= new Date(federatedCommunity.lastAnnouncedAt),
|
||||||
|
).length > 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon() {
|
||||||
|
return this.verified ? 'check' : 'x-circle'
|
||||||
|
},
|
||||||
|
variant() {
|
||||||
|
return this.verified ? 'success' : 'danger'
|
||||||
|
},
|
||||||
|
lastAnnouncedAt() {
|
||||||
|
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) return ''
|
||||||
|
const minDate = new Date(0)
|
||||||
|
const lastAnnouncedAt = this.item.federatedCommunities.reduce(
|
||||||
|
(lastAnnouncedAt, federateCommunity) => {
|
||||||
|
if (!federateCommunity.lastAnnouncedAt) return lastAnnouncedAt
|
||||||
|
const date = new Date(federateCommunity.lastAnnouncedAt)
|
||||||
|
return date > lastAnnouncedAt ? date : lastAnnouncedAt
|
||||||
|
},
|
||||||
|
minDate,
|
||||||
|
)
|
||||||
|
if (lastAnnouncedAt !== minDate) {
|
||||||
|
return formatDistanceToNow(lastAnnouncedAt, {
|
||||||
|
includeSecond: true,
|
||||||
|
addSuffix: true,
|
||||||
|
locale: locales[this.locale],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
createdAt() {
|
||||||
|
if (this.item.createdAt) {
|
||||||
|
return formatDistanceToNow(new Date(this.item.createdAt), {
|
||||||
|
includeSecond: true,
|
||||||
|
addSuffix: true,
|
||||||
|
locale: locales[this.locale],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleDetails() {
|
||||||
|
this.details = !this.details
|
||||||
|
},
|
||||||
|
handleSaveGsmApiKey(gmsApiKey) {
|
||||||
|
this.gmsApiKey = gmsApiKey
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: updateHomeCommunity,
|
||||||
|
variables: {
|
||||||
|
uuid: this.item.uuid,
|
||||||
|
gmsApiKey: gmsApiKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toastError(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
47
admin/src/components/Federation/FederationVisualizeItem.vue
Normal file
47
admin/src/components/Federation/FederationVisualizeItem.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="federation-visualize-item">
|
||||||
|
<b-row>
|
||||||
|
<b-col><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
|
||||||
|
<b-col class="ml-1">{{ item.apiVersion }}</b-col>
|
||||||
|
<b-col v-b-tooltip="item.createdAt">{{ distanceDate(item.createdAt) }}</b-col>
|
||||||
|
<b-col v-b-tooltip="item.lastAnnouncedAt">{{ distanceDate(item.lastAnnouncedAt) }}</b-col>
|
||||||
|
<b-col v-b-tooltip="item.verifiedAt">{{ distanceDate(item.verifiedAt) }}</b-col>
|
||||||
|
<b-col v-b-tooltip="item.lastErrorAt">{{ distanceDate(item.lastErrorAt) }}</b-col>
|
||||||
|
</b-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||||
|
|
||||||
|
const locales = { en, de, es, fr, nl }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FederationVisualizeItem',
|
||||||
|
props: {
|
||||||
|
item: { type: Object },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
verified() {
|
||||||
|
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
|
||||||
|
},
|
||||||
|
icon() {
|
||||||
|
return this.verified ? 'check' : 'x-circle'
|
||||||
|
},
|
||||||
|
variant() {
|
||||||
|
return this.verified ? 'success' : 'danger'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
distanceDate(dateString) {
|
||||||
|
return dateString
|
||||||
|
? formatDistanceToNow(new Date(dateString), {
|
||||||
|
includeSecond: true,
|
||||||
|
addSuffix: true,
|
||||||
|
locale: locales[this.$i18n.locale],
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
83
admin/src/components/input/EditableLabel.spec.js
Normal file
83
admin/src/components/input/EditableLabel.spec.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Test written by ChatGPT 3.5
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import EditableLabel from './EditableLabel.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
const value = 'test label value'
|
||||||
|
|
||||||
|
describe('EditableLabel', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const createWrapper = (propsData) => {
|
||||||
|
return mount(EditableLabel, {
|
||||||
|
localVue,
|
||||||
|
propsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders the label when not editing', () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
expect(wrapper.find('label').text()).toBe(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the input when editing', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('input').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits save event when clicking save button', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
await wrapper.setData({ inputValue: 'New Value' })
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.emitted().save).toBeTruthy()
|
||||||
|
expect(wrapper.emitted().save[0][0]).toBe('New Value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables save button when value is not changed', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enables save button when value is changed', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
await wrapper.setData({ inputValue: 'New Value' })
|
||||||
|
|
||||||
|
expect(wrapper.find('button').attributes('disabled')).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates originalValue when saving changes', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
await wrapper.setData({ inputValue: 'New Value' })
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.vm.originalValue).toBe('New Value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('changes variant to success when editing', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
await wrapper.find('button').trigger('click')
|
||||||
|
|
||||||
|
expect(wrapper.vm.variant).toBe('success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('changes variant to prime when not editing', async () => {
|
||||||
|
wrapper = createWrapper({ value, allowEdit: true })
|
||||||
|
|
||||||
|
expect(wrapper.vm.variant).toBe('prime')
|
||||||
|
})
|
||||||
|
})
|
||||||
64
admin/src/components/input/EditableLabel.vue
Normal file
64
admin/src/components/input/EditableLabel.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-form-group>
|
||||||
|
<label v-if="!editing">{{ value }}</label>
|
||||||
|
<b-form-input v-else v-model="inputValue" :placeholder="placeholder" />
|
||||||
|
</b-form-group>
|
||||||
|
<b-button
|
||||||
|
v-if="allowEdit"
|
||||||
|
@click="toggleEdit"
|
||||||
|
:disabled="!isValueChanged && editing"
|
||||||
|
:variant="variant"
|
||||||
|
>
|
||||||
|
<b-icon v-if="!editing" icon="pencil-fill" tooltip="$t('edit')"></b-icon>
|
||||||
|
<b-icon v-else icon="check" tooltip="$t('save')"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// Code written from chatGPT 3.5
|
||||||
|
name: 'EditableLabel',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
allowEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editing: false,
|
||||||
|
inputValue: this.value,
|
||||||
|
originalValue: this.value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
variant() {
|
||||||
|
return this.editing ? 'success' : 'prime'
|
||||||
|
},
|
||||||
|
isValueChanged() {
|
||||||
|
return this.inputValue !== this.originalValue
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleEdit() {
|
||||||
|
if (this.editing) {
|
||||||
|
this.$emit('save', this.inputValue)
|
||||||
|
this.originalValue = this.inputValue
|
||||||
|
}
|
||||||
|
this.editing = !this.editing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
29
admin/src/graphql/allCommunities.js
Normal file
29
admin/src/graphql/allCommunities.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const allCommunities = gql`
|
||||||
|
query {
|
||||||
|
allCommunities {
|
||||||
|
foreign
|
||||||
|
url
|
||||||
|
publicKey
|
||||||
|
uuid
|
||||||
|
authenticatedAt
|
||||||
|
name
|
||||||
|
description
|
||||||
|
gmsApiKey
|
||||||
|
creationDate
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
federatedCommunities {
|
||||||
|
id
|
||||||
|
apiVersion
|
||||||
|
endPoint
|
||||||
|
lastAnnouncedAt
|
||||||
|
verifiedAt
|
||||||
|
lastErrorAt
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import gql from 'graphql-tag'
|
|
||||||
|
|
||||||
export const getCommunities = gql`
|
|
||||||
query {
|
|
||||||
getCommunities {
|
|
||||||
id
|
|
||||||
foreign
|
|
||||||
publicKey
|
|
||||||
url
|
|
||||||
lastAnnouncedAt
|
|
||||||
verifiedAt
|
|
||||||
lastErrorAt
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
9
admin/src/graphql/updateHomeCommunity.js
Normal file
9
admin/src/graphql/updateHomeCommunity.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const updateHomeCommunity = gql`
|
||||||
|
mutation ($uuid: String!, $gmsApiKey: String!) {
|
||||||
|
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -73,11 +73,20 @@
|
|||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"expired": "abgelaufen",
|
"expired": "abgelaufen",
|
||||||
"federation": {
|
"federation": {
|
||||||
|
"apiVersion": "API Version",
|
||||||
|
"authenticatedAt": "Verifiziert am:",
|
||||||
|
"communityUuid": "Community UUID:",
|
||||||
"createdAt": "Erstellt am",
|
"createdAt": "Erstellt am",
|
||||||
|
"gmsApiKey": "GMS API Key:",
|
||||||
|
"toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!",
|
||||||
"gradidoInstances": "Gradido Instanzen",
|
"gradidoInstances": "Gradido Instanzen",
|
||||||
"lastAnnouncedAt": "letzte Bekanntgabe",
|
"lastAnnouncedAt": "letzte Bekanntgabe",
|
||||||
|
"lastErrorAt": "Letzer Fehler am",
|
||||||
|
"name": "Name",
|
||||||
|
"publicKey": "PublicKey:",
|
||||||
"url": "Url",
|
"url": "Url",
|
||||||
"verified": "Verifiziert"
|
"verified": "Verifiziert",
|
||||||
|
"verifiedAt": "Verifiziert am"
|
||||||
},
|
},
|
||||||
"firstname": "Vorname",
|
"firstname": "Vorname",
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@ -73,11 +73,20 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"expired": "expired",
|
"expired": "expired",
|
||||||
"federation": {
|
"federation": {
|
||||||
|
"apiVersion": "API Version",
|
||||||
|
"authenticatedAt": "verified at:",
|
||||||
|
"communityUuid": "Community UUID:",
|
||||||
"createdAt": "Created At ",
|
"createdAt": "Created At ",
|
||||||
|
"gmsApiKey": "GMS API Key:",
|
||||||
|
"toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!",
|
||||||
"gradidoInstances": "Gradido Instances",
|
"gradidoInstances": "Gradido Instances",
|
||||||
"lastAnnouncedAt": "Last Announced",
|
"lastAnnouncedAt": "Last Announced",
|
||||||
|
"lastErrorAt": "last error at",
|
||||||
|
"name": "Name",
|
||||||
|
"publicKey": "PublicKey:",
|
||||||
"url": "Url",
|
"url": "Url",
|
||||||
"verified": "Verified"
|
"verified": "Verified",
|
||||||
|
"verifiedAt": "Verified at"
|
||||||
},
|
},
|
||||||
"firstname": "Firstname",
|
"firstname": "Firstname",
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
|||||||
import FederationVisualize from './FederationVisualize'
|
import FederationVisualize from './FederationVisualize'
|
||||||
import VueApollo from 'vue-apollo'
|
import VueApollo from 'vue-apollo'
|
||||||
import { createMockClient } from 'mock-apollo-client'
|
import { createMockClient } from 'mock-apollo-client'
|
||||||
import { getCommunities } from '@/graphql/getCommunities'
|
import { allCommunities } from '@/graphql/allCommunities'
|
||||||
import { toastErrorSpy } from '../../test/testSetup'
|
import { toastErrorSpy } from '../../test/testSetup'
|
||||||
|
|
||||||
const mockClient = createMockClient()
|
const mockClient = createMockClient()
|
||||||
@ -25,42 +25,54 @@ const mocks = {
|
|||||||
|
|
||||||
const defaultData = () => {
|
const defaultData = () => {
|
||||||
return {
|
return {
|
||||||
getCommunities: [
|
allCommunities: [
|
||||||
{
|
{
|
||||||
id: 1776,
|
id: 1,
|
||||||
foreign: true,
|
foreign: false,
|
||||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
url: 'http://localhost/api/',
|
||||||
url: 'http://localhost/api/2_0',
|
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
|
||||||
lastAnnouncedAt: '2023-04-07T12:27:24.037Z',
|
communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21',
|
||||||
verifiedAt: null,
|
authenticatedAt: null,
|
||||||
lastErrorAt: null,
|
name: 'Gradido Test',
|
||||||
createdAt: '2023-04-07T11:45:06.254Z',
|
description: 'Gradido Community zum testen',
|
||||||
updatedAt: null,
|
gmsApiKey: '<api key>',
|
||||||
__typename: 'Community',
|
creationDate: '2024-01-09T15:56:40.592Z',
|
||||||
},
|
createdAt: '2024-01-09T15:56:40.595Z',
|
||||||
{
|
updatedAt: '2024-01-16T11:17:15.000Z',
|
||||||
id: 1775,
|
federatedCommunities: [
|
||||||
foreign: true,
|
{
|
||||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
id: 2046,
|
||||||
url: 'http://localhost/api/1_1',
|
apiVersion: '2_0',
|
||||||
lastAnnouncedAt: '2023-04-07T12:27:24.023Z',
|
endPoint: 'http://localhost/api/',
|
||||||
verifiedAt: null,
|
lastAnnouncedAt: null,
|
||||||
lastErrorAt: null,
|
verifiedAt: null,
|
||||||
createdAt: '2023-04-07T11:45:06.234Z',
|
lastErrorAt: null,
|
||||||
updatedAt: null,
|
createdAt: '2024-01-16T10:08:21.544Z',
|
||||||
__typename: 'Community',
|
updatedAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1774,
|
id: 2045,
|
||||||
foreign: true,
|
apiVersion: '1_1',
|
||||||
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
|
endPoint: 'http://localhost/api/',
|
||||||
url: 'http://localhost/api/1_0',
|
lastAnnouncedAt: null,
|
||||||
lastAnnouncedAt: '2023-04-07T12:27:24.009Z',
|
verifiedAt: null,
|
||||||
verifiedAt: null,
|
lastErrorAt: null,
|
||||||
lastErrorAt: null,
|
createdAt: '2024-01-16T10:08:21.550Z',
|
||||||
createdAt: '2023-04-07T11:45:06.218Z',
|
updatedAt: null,
|
||||||
updatedAt: null,
|
__typename: 'FederatedCommunity',
|
||||||
__typename: 'Community',
|
},
|
||||||
|
{
|
||||||
|
id: 2044,
|
||||||
|
apiVersion: '1_0',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
createdAt: '2024-01-16T10:08:21.544Z',
|
||||||
|
updatedAt: null,
|
||||||
|
__typename: 'FederatedCommunity',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -68,11 +80,11 @@ const defaultData = () => {
|
|||||||
|
|
||||||
describe('FederationVisualize', () => {
|
describe('FederationVisualize', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
const getCommunitiesMock = jest.fn()
|
const allCommunitiesMock = jest.fn()
|
||||||
|
|
||||||
mockClient.setRequestHandler(
|
mockClient.setRequestHandler(
|
||||||
getCommunities,
|
allCommunities,
|
||||||
getCommunitiesMock
|
allCommunitiesMock
|
||||||
.mockRejectedValueOnce({ message: 'Ouch!' })
|
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||||
.mockResolvedValue({ data: defaultData() }),
|
.mockResolvedValue({ data: defaultData() }),
|
||||||
)
|
)
|
||||||
@ -95,7 +107,7 @@ describe('FederationVisualize', () => {
|
|||||||
|
|
||||||
describe('sever success', () => {
|
describe('sever success', () => {
|
||||||
it('sends query to Apollo when created', () => {
|
it('sends query to Apollo when created', () => {
|
||||||
expect(getCommunitiesMock).toBeCalled()
|
expect(allCommunitiesMock).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a DIV element with the class "federation-visualize"', () => {
|
it('has a DIV element with the class "federation-visualize"', () => {
|
||||||
@ -106,8 +118,8 @@ describe('FederationVisualize', () => {
|
|||||||
expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true)
|
expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders 3 community list items', () => {
|
it('renders 1 community list item', () => {
|
||||||
expect(wrapper.findAll('.list-group-item').length).toBe(3)
|
expect(wrapper.findAll('.list-group-item').length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('cklicking the refresh button', () => {
|
describe('cklicking the refresh button', () => {
|
||||||
@ -117,7 +129,7 @@ describe('FederationVisualize', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls the API', async () => {
|
it('calls the API', async () => {
|
||||||
expect(getCommunitiesMock).toBeCalled()
|
expect(allCommunitiesMock).toBeCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
icon="arrow-clockwise"
|
icon="arrow-clockwise"
|
||||||
font-scale="2"
|
font-scale="2"
|
||||||
:animation="animation"
|
:animation="animation"
|
||||||
@click="$apollo.queries.GetCommunities.refresh()"
|
@click="$apollo.queries.allCommunities.refresh()"
|
||||||
data-test="federation-communities-refresh-btn"
|
data-test="federation-communities-refresh-btn"
|
||||||
></b-icon>
|
></b-icon>
|
||||||
</b-button>
|
</b-button>
|
||||||
@ -16,28 +16,29 @@
|
|||||||
<b-row>
|
<b-row>
|
||||||
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
|
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
|
||||||
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
|
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
|
||||||
|
<b-col class="ml-3">{{ $t('federation.name') }}</b-col>
|
||||||
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
|
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
|
||||||
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
|
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
<b-list-group-item
|
<b-list-group-item
|
||||||
v-for="item in communities"
|
v-for="item in communities"
|
||||||
:key="item.id"
|
:key="item.publicKey"
|
||||||
:variant="!item.foreign ? 'primary' : 'warning'"
|
:variant="!item.foreign ? 'primary' : 'warning'"
|
||||||
>
|
>
|
||||||
<federation-visualize-item :item="item" />
|
<community-visualize-item :item="item" />
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { getCommunities } from '@/graphql/getCommunities'
|
import { allCommunities } from '@/graphql/allCommunities'
|
||||||
|
|
||||||
import FederationVisualizeItem from '../components/Fedaration/FederationVisualizeItem.vue'
|
import CommunityVisualizeItem from '../components/Federation/CommunityVisualizeItem.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FederationVisualize',
|
name: 'FederationVisualize',
|
||||||
components: {
|
components: {
|
||||||
FederationVisualizeItem,
|
CommunityVisualizeItem,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -48,17 +49,17 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
animation() {
|
animation() {
|
||||||
return this.$apollo.queries.GetCommunities.loading ? 'spin' : ''
|
return this.$apollo.queries.allCommunities.loading ? 'spin' : ''
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
GetCommunities: {
|
allCommunities: {
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
query() {
|
query() {
|
||||||
return getCommunities
|
return allCommunities
|
||||||
},
|
},
|
||||||
update({ getCommunities }) {
|
update({ allCommunities }) {
|
||||||
this.communities = getCommunities
|
this.communities = allCommunities
|
||||||
},
|
},
|
||||||
error({ message }) {
|
error({ message }) {
|
||||||
this.toastError(message)
|
this.toastError(message)
|
||||||
|
|||||||
1
backend/.nvmrc
Normal file
1
backend/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
v18.7.0
|
||||||
@ -16,6 +16,7 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@/(.*)': '<rootDir>/src/$1',
|
'@/(.*)': '<rootDir>/src/$1',
|
||||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||||
|
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
|
||||||
'@dltConnector/(.*)': '<rootDir>/src/apis/dltConnector/$1',
|
'@dltConnector/(.*)': '<rootDir>/src/apis/dltConnector/$1',
|
||||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||||
|
|||||||
@ -6,4 +6,6 @@ export const ADMIN_RIGHTS = [
|
|||||||
RIGHTS.UNDELETE_USER,
|
RIGHTS.UNDELETE_USER,
|
||||||
RIGHTS.COMMUNITY_UPDATE,
|
RIGHTS.COMMUNITY_UPDATE,
|
||||||
RIGHTS.COMMUNITY_BY_UUID,
|
RIGHTS.COMMUNITY_BY_UUID,
|
||||||
|
RIGHTS.COMMUNITY_BY_IDENTIFIER,
|
||||||
|
RIGHTS.HOME_COMMUNITY,
|
||||||
]
|
]
|
||||||
|
|||||||
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
3
backend/src/auth/DLT_CONNECTOR_RIGHTS.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { RIGHTS } from './RIGHTS'
|
||||||
|
|
||||||
|
export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER, RIGHTS.HOME_COMMUNITY]
|
||||||
@ -59,5 +59,7 @@ export enum RIGHTS {
|
|||||||
DELETE_USER = 'DELETE_USER',
|
DELETE_USER = 'DELETE_USER',
|
||||||
UNDELETE_USER = 'UNDELETE_USER',
|
UNDELETE_USER = 'UNDELETE_USER',
|
||||||
COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID',
|
COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID',
|
||||||
|
COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER',
|
||||||
|
HOME_COMMUNITY = 'HOME_COMMUNITY',
|
||||||
COMMUNITY_UPDATE = 'COMMUNITY_UPDATE',
|
COMMUNITY_UPDATE = 'COMMUNITY_UPDATE',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { RoleNames } from '@/graphql/enum/RoleNames'
|
import { RoleNames } from '@/graphql/enum/RoleNames'
|
||||||
|
|
||||||
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
|
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
|
||||||
|
import { DLT_CONNECTOR_RIGHTS } from './DLT_CONNECTOR_RIGHTS'
|
||||||
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
|
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
|
||||||
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
|
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
|
||||||
import { Role } from './Role'
|
import { Role } from './Role'
|
||||||
@ -20,5 +21,7 @@ export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [
|
|||||||
...ADMIN_RIGHTS,
|
...ADMIN_RIGHTS,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const ROLE_DLT_CONNECTOR = new Role(RoleNames.DLT_CONNECTOR, DLT_CONNECTOR_RIGHTS)
|
||||||
|
|
||||||
// TODO from database
|
// TODO from database
|
||||||
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]
|
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]
|
||||||
|
|||||||
@ -12,7 +12,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0082-introduce_gms_registration',
|
DB_VERSION: '0083-join_community_federated_communities',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { IsString } from 'class-validator'
|
import { IsBoolean, IsString } from 'class-validator'
|
||||||
import { Field, ArgsType, InputType } from 'type-graphql'
|
import { ArgsType, Field } from 'type-graphql'
|
||||||
|
|
||||||
@InputType()
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class CommunityArgs {
|
export class CommunityArgs {
|
||||||
@Field(() => String)
|
@Field(() => String, { nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
uuid: string
|
communityIdentifier?: string | null
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => Boolean, { nullable: true })
|
||||||
@IsString()
|
@IsBoolean()
|
||||||
gmsApiKey: string
|
foreign?: boolean | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { IsBoolean, IsInt, IsString } from 'class-validator'
|
import { IsBoolean, IsEnum, IsInt, IsString } from 'class-validator'
|
||||||
import { ArgsType, Field, InputType, Int } from 'type-graphql'
|
import { ArgsType, Field, InputType, Int } from 'type-graphql'
|
||||||
|
|
||||||
|
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||||
|
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||||
import { Location } from '@model/Location'
|
import { Location } from '@model/Location'
|
||||||
|
|
||||||
import { isValidLocation } from '@/graphql/validator/Location'
|
import { isValidLocation } from '@/graphql/validator/Location'
|
||||||
@ -44,19 +46,19 @@ export class UpdateUserInfosArgs {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
hideAmountGDT?: boolean
|
hideAmountGDT?: boolean
|
||||||
|
|
||||||
@Field({ nullable: true, defaultValue: true })
|
@Field({ nullable: true })
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
gmsAllowed?: boolean
|
gmsAllowed?: boolean
|
||||||
|
|
||||||
@Field(() => Int, { nullable: true, defaultValue: 0 })
|
@Field(() => GmsPublishNameType, { nullable: true })
|
||||||
@IsInt()
|
@IsEnum(GmsPublishNameType)
|
||||||
gmsPublishName?: number | null
|
gmsPublishName?: GmsPublishNameType | null
|
||||||
|
|
||||||
@Field(() => Location, { nullable: true })
|
@Field(() => Location, { nullable: true })
|
||||||
@isValidLocation()
|
@isValidLocation()
|
||||||
gmsLocation?: Location | null
|
gmsLocation?: Location | null
|
||||||
|
|
||||||
@Field(() => Int, { nullable: true, defaultValue: 2 })
|
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||||
@IsInt()
|
@IsEnum(GmsPublishLocationType)
|
||||||
gmsPublishLocation?: number | null
|
gmsPublishLocation?: GmsPublishLocationType | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,13 @@ import { RoleNames } from '@enum/RoleNames'
|
|||||||
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
||||||
import { decode, encode } from '@/auth/JWT'
|
import { decode, encode } from '@/auth/JWT'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES'
|
import {
|
||||||
|
ROLE_UNAUTHORIZED,
|
||||||
|
ROLE_USER,
|
||||||
|
ROLE_ADMIN,
|
||||||
|
ROLE_MODERATOR,
|
||||||
|
ROLE_DLT_CONNECTOR,
|
||||||
|
} from '@/auth/ROLES'
|
||||||
import { Context } from '@/server/context'
|
import { Context } from '@/server/context'
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
@ -30,31 +36,35 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
|
|||||||
// Set context gradidoID
|
// Set context gradidoID
|
||||||
context.gradidoID = decoded.gradidoID
|
context.gradidoID = decoded.gradidoID
|
||||||
|
|
||||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
if (context.gradidoID === 'dlt-connector') {
|
||||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
context.role = ROLE_DLT_CONNECTOR
|
||||||
try {
|
} else {
|
||||||
const user = await User.findOneOrFail({
|
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||||
where: { gradidoID: decoded.gradidoID },
|
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||||
withDeleted: true,
|
try {
|
||||||
relations: ['emailContact', 'userRoles'],
|
const user = await User.findOneOrFail({
|
||||||
})
|
where: { gradidoID: decoded.gradidoID },
|
||||||
context.user = user
|
withDeleted: true,
|
||||||
context.role = ROLE_USER
|
relations: ['emailContact', 'userRoles'],
|
||||||
if (user.userRoles?.length > 0) {
|
})
|
||||||
switch (user.userRoles[0].role) {
|
context.user = user
|
||||||
case RoleNames.ADMIN:
|
context.role = ROLE_USER
|
||||||
context.role = ROLE_ADMIN
|
if (user.userRoles?.length > 0) {
|
||||||
break
|
switch (user.userRoles[0].role) {
|
||||||
case RoleNames.MODERATOR:
|
case RoleNames.ADMIN:
|
||||||
context.role = ROLE_MODERATOR
|
context.role = ROLE_ADMIN
|
||||||
break
|
break
|
||||||
default:
|
case RoleNames.MODERATOR:
|
||||||
context.role = ROLE_USER
|
context.role = ROLE_MODERATOR
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
context.role = ROLE_USER
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// in case the database query fails (user deleted)
|
||||||
|
throw new LogError('401 Unauthorized')
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// in case the database query fails (user deleted)
|
|
||||||
throw new LogError('401 Unauthorized')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for correct rights
|
// check for correct rights
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum RoleNames {
|
|||||||
USER = 'USER',
|
USER = 'USER',
|
||||||
MODERATOR = 'MODERATOR',
|
MODERATOR = 'MODERATOR',
|
||||||
ADMIN = 'ADMIN',
|
ADMIN = 'ADMIN',
|
||||||
|
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(RoleNames, {
|
registerEnumType(RoleNames, {
|
||||||
|
|||||||
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
14
backend/src/graphql/input/EditCommunityInput.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { IsString, IsUUID } from 'class-validator'
|
||||||
|
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
@InputType()
|
||||||
|
export class EditCommunityInput {
|
||||||
|
@Field(() => String)
|
||||||
|
@IsUUID('4')
|
||||||
|
uuid: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
@IsString()
|
||||||
|
gmsApiKey: string
|
||||||
|
}
|
||||||
76
backend/src/graphql/model/AdminCommunityView.ts
Normal file
76
backend/src/graphql/model/AdminCommunityView.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
|
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
|
||||||
|
import { FederatedCommunity } from './FederatedCommunity'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AdminCommunityView {
|
||||||
|
constructor(dbCom: DbCommunity) {
|
||||||
|
if (dbCom.federatedCommunities && dbCom.federatedCommunities.length > 0) {
|
||||||
|
const federatedCommunity = dbCom.federatedCommunities[0]
|
||||||
|
this.foreign = federatedCommunity.foreign
|
||||||
|
const url = new URL(federatedCommunity.endPoint)
|
||||||
|
// use only the host part
|
||||||
|
this.url = url.protocol + '//' + url.host
|
||||||
|
this.publicKey = federatedCommunity.publicKey.toString('hex')
|
||||||
|
this.federatedCommunities = dbCom.federatedCommunities.map(
|
||||||
|
(federatedCom: DbFederatedCommunity) => new FederatedCommunity(federatedCom),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (dbCom.foreign !== undefined) {
|
||||||
|
this.foreign = dbCom.foreign
|
||||||
|
}
|
||||||
|
this.name = dbCom.name
|
||||||
|
this.description = dbCom.description
|
||||||
|
this.gmsApiKey = dbCom.gmsApiKey
|
||||||
|
if (dbCom.url) {
|
||||||
|
this.url = dbCom.url
|
||||||
|
}
|
||||||
|
if (dbCom.publicKey && dbCom.publicKey.length === 32) {
|
||||||
|
this.publicKey = dbCom.publicKey.toString('hex')
|
||||||
|
}
|
||||||
|
this.creationDate = dbCom.creationDate
|
||||||
|
this.createdAt = dbCom.createdAt
|
||||||
|
this.updatedAt = dbCom.updatedAt
|
||||||
|
this.uuid = dbCom.communityUuid
|
||||||
|
this.authenticatedAt = dbCom.authenticatedAt
|
||||||
|
this.gmsApiKey = dbCom.gmsApiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
foreign: boolean
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
url: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
publicKey: string
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
uuid: string | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
authenticatedAt: Date | null
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
name: string | null
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
description: string | null
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
gmsApiKey: string | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
creationDate: Date | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
createdAt: Date | null
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@Field(() => [FederatedCommunity], { nullable: true })
|
||||||
|
federatedCommunities: FederatedCommunity[] | null
|
||||||
|
}
|
||||||
@ -7,8 +7,8 @@ export class FederatedCommunity {
|
|||||||
this.id = dbCom.id
|
this.id = dbCom.id
|
||||||
this.foreign = dbCom.foreign
|
this.foreign = dbCom.foreign
|
||||||
this.publicKey = dbCom.publicKey.toString('hex')
|
this.publicKey = dbCom.publicKey.toString('hex')
|
||||||
this.url =
|
this.apiVersion = dbCom.apiVersion
|
||||||
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
|
this.endPoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
|
||||||
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
|
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
|
||||||
this.verifiedAt = dbCom.verifiedAt
|
this.verifiedAt = dbCom.verifiedAt
|
||||||
this.lastErrorAt = dbCom.lastErrorAt
|
this.lastErrorAt = dbCom.lastErrorAt
|
||||||
@ -26,7 +26,10 @@ export class FederatedCommunity {
|
|||||||
publicKey: string
|
publicKey: string
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
url: string
|
apiVersion: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
endPoint: string
|
||||||
|
|
||||||
@Field(() => Date, { nullable: true })
|
@Field(() => Date, { nullable: true })
|
||||||
lastAnnouncedAt: Date | null
|
lastAnnouncedAt: Date | null
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
|
|
||||||
|
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
||||||
|
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
||||||
|
|
||||||
import { KlickTipp } from './KlickTipp'
|
import { KlickTipp } from './KlickTipp'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@ -29,6 +32,9 @@ export class User {
|
|||||||
this.hasElopage = null
|
this.hasElopage = null
|
||||||
this.hideAmountGDD = user.hideAmountGDD
|
this.hideAmountGDD = user.hideAmountGDD
|
||||||
this.hideAmountGDT = user.hideAmountGDT
|
this.hideAmountGDT = user.hideAmountGDT
|
||||||
|
this.gmsAllowed = user.gmsAllowed
|
||||||
|
this.gmsPublishName = user.gmsPublishName
|
||||||
|
this.gmsPublishLocation = user.gmsPublishLocation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +80,15 @@ export class User {
|
|||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
hideAmountGDT: boolean
|
hideAmountGDT: boolean
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
gmsAllowed: boolean
|
||||||
|
|
||||||
|
@Field(() => GmsPublishNameType, { nullable: true })
|
||||||
|
gmsPublishName: GmsPublishNameType | null
|
||||||
|
|
||||||
|
@Field(() => GmsPublishLocationType, { nullable: true })
|
||||||
|
gmsPublishLocation: GmsPublishLocationType | null
|
||||||
|
|
||||||
// This is not the users publisherId, but the one of the users who recommend him
|
// This is not the users publisherId, but the one of the users who recommend him
|
||||||
@Field(() => Int, { nullable: true })
|
@Field(() => Int, { nullable: true })
|
||||||
publisherId: number | null
|
publisherId: number | null
|
||||||
|
|||||||
@ -10,13 +10,20 @@ import { Community as DbCommunity } from '@entity/Community'
|
|||||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { GraphQLError } from 'graphql/error/GraphQLError'
|
import { GraphQLError } from 'graphql/error/GraphQLError'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||||
import { logger, i18n as localization } from '@test/testSetup'
|
import { logger, i18n as localization } from '@test/testSetup'
|
||||||
|
|
||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
|
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
|
||||||
import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries'
|
import {
|
||||||
|
allCommunities,
|
||||||
|
getCommunities,
|
||||||
|
communitiesQuery,
|
||||||
|
getHomeCommunityQuery,
|
||||||
|
getCommunityByIdentifierQuery,
|
||||||
|
} from '@/seeds/graphql/queries'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
|
||||||
import { getCommunityByUuid } from './util/communities'
|
import { getCommunityByUuid } from './util/communities'
|
||||||
@ -164,7 +171,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 3,
|
id: 3,
|
||||||
foreign: homeCom3.foreign,
|
foreign: homeCom3.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||||
url: expect.stringMatching('http://localhost/api/2_0'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '2_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -175,7 +183,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
foreign: homeCom2.foreign,
|
foreign: homeCom2.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||||
url: expect.stringMatching('http://localhost/api/1_1'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '1_1',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -186,7 +195,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
foreign: homeCom1.foreign,
|
foreign: homeCom1.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||||
url: expect.stringMatching('http://localhost/api/1_0'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '1_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -222,7 +232,7 @@ describe('CommunityResolver', () => {
|
|||||||
foreignCom3 = DbFederatedCommunity.create()
|
foreignCom3 = DbFederatedCommunity.create()
|
||||||
foreignCom3.foreign = true
|
foreignCom3.foreign = true
|
||||||
foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
|
foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
|
||||||
foreignCom3.apiVersion = '1_2'
|
foreignCom3.apiVersion = '2_0'
|
||||||
foreignCom3.endPoint = 'http://remotehost/api'
|
foreignCom3.endPoint = 'http://remotehost/api'
|
||||||
foreignCom3.createdAt = new Date()
|
foreignCom3.createdAt = new Date()
|
||||||
await DbFederatedCommunity.insert(foreignCom3)
|
await DbFederatedCommunity.insert(foreignCom3)
|
||||||
@ -236,7 +246,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 3,
|
id: 3,
|
||||||
foreign: homeCom3.foreign,
|
foreign: homeCom3.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||||
url: expect.stringMatching('http://localhost/api/2_0'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '2_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -247,7 +258,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
foreign: homeCom2.foreign,
|
foreign: homeCom2.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||||
url: expect.stringMatching('http://localhost/api/1_1'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '1_1',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -258,7 +270,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
foreign: homeCom1.foreign,
|
foreign: homeCom1.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||||
url: expect.stringMatching('http://localhost/api/1_0'),
|
endPoint: expect.stringMatching('http://localhost/api/'),
|
||||||
|
apiVersion: '1_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -269,7 +282,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 6,
|
id: 6,
|
||||||
foreign: foreignCom3.foreign,
|
foreign: foreignCom3.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
|
||||||
url: expect.stringMatching('http://remotehost/api/1_2'),
|
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||||
|
apiVersion: '2_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -280,7 +294,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 5,
|
id: 5,
|
||||||
foreign: foreignCom2.foreign,
|
foreign: foreignCom2.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
|
||||||
url: expect.stringMatching('http://remotehost/api/1_1'),
|
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||||
|
apiVersion: '1_1',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -291,7 +306,8 @@ describe('CommunityResolver', () => {
|
|||||||
id: 4,
|
id: 4,
|
||||||
foreign: foreignCom1.foreign,
|
foreign: foreignCom1.foreign,
|
||||||
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
|
||||||
url: expect.stringMatching('http://remotehost/api/1_0'),
|
endPoint: expect.stringMatching('http://remotehost/api/'),
|
||||||
|
apiVersion: '1_0',
|
||||||
lastAnnouncedAt: null,
|
lastAnnouncedAt: null,
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
lastErrorAt: null,
|
lastErrorAt: null,
|
||||||
@ -303,6 +319,229 @@ describe('CommunityResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with 6 federated community entries', () => {
|
||||||
|
let comHomeCom1: DbCommunity
|
||||||
|
let comForeignCom1: DbCommunity
|
||||||
|
let comForeignCom2: DbCommunity
|
||||||
|
let foreignCom4: DbFederatedCommunity
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
comHomeCom1 = DbCommunity.create()
|
||||||
|
comHomeCom1.foreign = false
|
||||||
|
comHomeCom1.url = 'http://localhost'
|
||||||
|
comHomeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex')
|
||||||
|
comHomeCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[0].private, 'hex')
|
||||||
|
comHomeCom1.communityUuid = 'HomeCom-UUID'
|
||||||
|
comHomeCom1.authenticatedAt = new Date()
|
||||||
|
comHomeCom1.name = 'HomeCommunity-name'
|
||||||
|
comHomeCom1.description = 'HomeCommunity-description'
|
||||||
|
comHomeCom1.creationDate = new Date()
|
||||||
|
await DbCommunity.insert(comHomeCom1)
|
||||||
|
|
||||||
|
comForeignCom1 = DbCommunity.create()
|
||||||
|
comForeignCom1.foreign = true
|
||||||
|
comForeignCom1.url = 'http://stage-2.gradido.net'
|
||||||
|
comForeignCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[3].public, 'hex')
|
||||||
|
comForeignCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[3].private, 'hex')
|
||||||
|
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
||||||
|
// foreignCom1.authenticatedAt = new Date()
|
||||||
|
comForeignCom1.name = 'Stage-2_Community-name'
|
||||||
|
comForeignCom1.description = 'Stage-2_Community-description'
|
||||||
|
comForeignCom1.creationDate = new Date()
|
||||||
|
await DbCommunity.insert(comForeignCom1)
|
||||||
|
|
||||||
|
comForeignCom2 = DbCommunity.create()
|
||||||
|
comForeignCom2.foreign = true
|
||||||
|
comForeignCom2.url = 'http://stage-3.gradido.net'
|
||||||
|
comForeignCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[4].public, 'hex')
|
||||||
|
comForeignCom2.privateKey = Buffer.from(ed25519KeyPairStaticHex[4].private, 'hex')
|
||||||
|
comForeignCom2.communityUuid = 'Stage3-Com-UUID'
|
||||||
|
comForeignCom2.authenticatedAt = new Date()
|
||||||
|
comForeignCom2.name = 'Stage-3_Community-name'
|
||||||
|
comForeignCom2.description = 'Stage-3_Community-description'
|
||||||
|
comForeignCom2.creationDate = new Date()
|
||||||
|
await DbCommunity.insert(comForeignCom2)
|
||||||
|
|
||||||
|
foreignCom4 = DbFederatedCommunity.create()
|
||||||
|
foreignCom4.foreign = true
|
||||||
|
foreignCom4.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex')
|
||||||
|
foreignCom4.apiVersion = '1_0'
|
||||||
|
foreignCom4.endPoint = 'http://remotehost/api'
|
||||||
|
foreignCom4.createdAt = new Date()
|
||||||
|
await DbFederatedCommunity.insert(foreignCom4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('return communities structured for admin ', async () => {
|
||||||
|
await expect(query({ query: allCommunities })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
allCommunities: [
|
||||||
|
{
|
||||||
|
foreign: false,
|
||||||
|
url: 'http://localhost',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public),
|
||||||
|
authenticatedAt: null,
|
||||||
|
createdAt: null,
|
||||||
|
creationDate: null,
|
||||||
|
description: null,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: null,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: null,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
apiVersion: '2_0',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
createdAt: homeCom3.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreign: false,
|
||||||
|
url: 'http://localhost',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public),
|
||||||
|
authenticatedAt: null,
|
||||||
|
createdAt: null,
|
||||||
|
creationDate: null,
|
||||||
|
description: null,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: null,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: null,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
apiVersion: '1_1',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
createdAt: homeCom2.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreign: false,
|
||||||
|
url: 'http://localhost',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public),
|
||||||
|
authenticatedAt: comHomeCom1.authenticatedAt?.toISOString(),
|
||||||
|
createdAt: comHomeCom1.createdAt.toISOString(),
|
||||||
|
creationDate: comHomeCom1.creationDate?.toISOString(),
|
||||||
|
description: comHomeCom1.description,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: comHomeCom1.name,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: comHomeCom1.communityUuid,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
apiVersion: '1_0',
|
||||||
|
endPoint: 'http://localhost/api/',
|
||||||
|
createdAt: homeCom1.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreign: true,
|
||||||
|
url: 'http://remotehost',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public),
|
||||||
|
authenticatedAt: null,
|
||||||
|
createdAt: null,
|
||||||
|
creationDate: null,
|
||||||
|
description: null,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: null,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: null,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
apiVersion: '1_0',
|
||||||
|
endPoint: 'http://remotehost/api/',
|
||||||
|
createdAt: foreignCom4.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
apiVersion: '2_0',
|
||||||
|
endPoint: 'http://remotehost/api/',
|
||||||
|
createdAt: foreignCom3.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreign: true,
|
||||||
|
url: 'http://stage-3.gradido.net',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public),
|
||||||
|
authenticatedAt: comForeignCom2.authenticatedAt?.toISOString(),
|
||||||
|
createdAt: comForeignCom2.createdAt.toISOString(),
|
||||||
|
creationDate: comForeignCom2.creationDate?.toISOString(),
|
||||||
|
description: comForeignCom2.description,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: comForeignCom2.name,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: comForeignCom2.communityUuid,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
apiVersion: '1_1',
|
||||||
|
endPoint: 'http://remotehost/api/',
|
||||||
|
createdAt: foreignCom2.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreign: true,
|
||||||
|
url: 'http://stage-2.gradido.net',
|
||||||
|
publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public),
|
||||||
|
authenticatedAt: null,
|
||||||
|
createdAt: comForeignCom1.createdAt.toISOString(),
|
||||||
|
creationDate: comForeignCom1.creationDate?.toISOString(),
|
||||||
|
description: comForeignCom1.description,
|
||||||
|
gmsApiKey: null,
|
||||||
|
name: comForeignCom1.name,
|
||||||
|
updatedAt: null,
|
||||||
|
uuid: null,
|
||||||
|
federatedCommunities: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
apiVersion: '1_0',
|
||||||
|
endPoint: 'http://remotehost/api/',
|
||||||
|
createdAt: foreignCom1.createdAt.toISOString(),
|
||||||
|
lastAnnouncedAt: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
verifiedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('communities', () => {
|
describe('communities', () => {
|
||||||
@ -464,8 +703,14 @@ describe('CommunityResolver', () => {
|
|||||||
foreignCom1 = DbCommunity.create()
|
foreignCom1 = DbCommunity.create()
|
||||||
foreignCom1.foreign = true
|
foreignCom1.foreign = true
|
||||||
foreignCom1.url = 'http://stage-2.gradido.net/api'
|
foreignCom1.url = 'http://stage-2.gradido.net/api'
|
||||||
foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community')
|
foreignCom1.publicKey = Buffer.from(
|
||||||
foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community')
|
'8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d',
|
||||||
|
'hex',
|
||||||
|
)
|
||||||
|
foreignCom1.privateKey = Buffer.from(
|
||||||
|
'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a',
|
||||||
|
'hex',
|
||||||
|
)
|
||||||
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
// foreignCom1.communityUuid = 'Stage2-Com-UUID'
|
||||||
// foreignCom1.authenticatedAt = new Date()
|
// foreignCom1.authenticatedAt = new Date()
|
||||||
foreignCom1.name = 'Stage-2_Community-name'
|
foreignCom1.name = 'Stage-2_Community-name'
|
||||||
@ -476,9 +721,15 @@ describe('CommunityResolver', () => {
|
|||||||
foreignCom2 = DbCommunity.create()
|
foreignCom2 = DbCommunity.create()
|
||||||
foreignCom2.foreign = true
|
foreignCom2.foreign = true
|
||||||
foreignCom2.url = 'http://stage-3.gradido.net/api'
|
foreignCom2.url = 'http://stage-3.gradido.net/api'
|
||||||
foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community')
|
foreignCom2.publicKey = Buffer.from(
|
||||||
foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community')
|
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
|
||||||
foreignCom2.communityUuid = 'Stage3-Com-UUID'
|
'hex',
|
||||||
|
)
|
||||||
|
foreignCom2.privateKey = Buffer.from(
|
||||||
|
'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f',
|
||||||
|
'hex',
|
||||||
|
)
|
||||||
|
foreignCom2.communityUuid = uuidv4()
|
||||||
foreignCom2.authenticatedAt = new Date()
|
foreignCom2.authenticatedAt = new Date()
|
||||||
foreignCom2.name = 'Stage-3_Community-name'
|
foreignCom2.name = 'Stage-3_Community-name'
|
||||||
foreignCom2.description = 'Stage-3_Community-description'
|
foreignCom2.description = 'Stage-3_Community-description'
|
||||||
@ -486,15 +737,36 @@ describe('CommunityResolver', () => {
|
|||||||
await DbCommunity.insert(foreignCom2)
|
await DbCommunity.insert(foreignCom2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('finds the home-community', async () => {
|
it('finds the home-community by uuid', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: getCommunityByUuidQuery,
|
query: getCommunityByIdentifierQuery,
|
||||||
variables: { communityUuid: homeCom?.communityUuid },
|
variables: { communityIdentifier: homeCom?.communityUuid },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
community: {
|
communityByIdentifier: {
|
||||||
|
id: homeCom?.id,
|
||||||
|
foreign: homeCom?.foreign,
|
||||||
|
name: homeCom?.name,
|
||||||
|
description: homeCom?.description,
|
||||||
|
url: homeCom?.url,
|
||||||
|
creationDate: homeCom?.creationDate?.toISOString(),
|
||||||
|
uuid: homeCom?.communityUuid,
|
||||||
|
authenticatedAt: homeCom?.authenticatedAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds the home-community', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: getHomeCommunityQuery,
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
homeCommunity: {
|
||||||
id: homeCom?.id,
|
id: homeCom?.id,
|
||||||
foreign: homeCom?.foreign,
|
foreign: homeCom?.foreign,
|
||||||
name: homeCom?.name,
|
name: homeCom?.name,
|
||||||
@ -563,7 +835,7 @@ describe('CommunityResolver', () => {
|
|||||||
expect(
|
expect(
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: updateHomeCommunityQuery,
|
mutation: updateHomeCommunityQuery,
|
||||||
variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' },
|
variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' },
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import { IsNull, Not } from '@dbTools/typeorm'
|
import { IsNull, Not } from '@dbTools/typeorm'
|
||||||
import { Community as DbCommunity } from '@entity/Community'
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||||
import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql'
|
import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql'
|
||||||
|
|
||||||
import { CommunityArgs } from '@arg//CommunityArgs'
|
import { Paginated } from '@arg/Paginated'
|
||||||
|
import { EditCommunityInput } from '@input/EditCommunityInput'
|
||||||
|
import { AdminCommunityView } from '@model/AdminCommunityView'
|
||||||
import { Community } from '@model/Community'
|
import { Community } from '@model/Community'
|
||||||
import { FederatedCommunity } from '@model/FederatedCommunity'
|
import { FederatedCommunity } from '@model/FederatedCommunity'
|
||||||
|
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
import { getCommunityByUuid } from './util/communities'
|
import {
|
||||||
|
getAllCommunities,
|
||||||
|
getCommunityByIdentifier,
|
||||||
|
getCommunityByUuid,
|
||||||
|
getHomeCommunity,
|
||||||
|
} from './util/communities'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class CommunityResolver {
|
export class CommunityResolver {
|
||||||
@ -29,6 +36,12 @@ export class CommunityResolver {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.COMMUNITIES])
|
||||||
|
@Query(() => [AdminCommunityView])
|
||||||
|
async allCommunities(@Args() paginated: Paginated): Promise<AdminCommunityView[]> {
|
||||||
|
return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom))
|
||||||
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.COMMUNITIES])
|
@Authorized([RIGHTS.COMMUNITIES])
|
||||||
@Query(() => [Community])
|
@Query(() => [Community])
|
||||||
async communities(): Promise<Community[]> {
|
async communities(): Promise<Community[]> {
|
||||||
@ -41,41 +54,42 @@ export class CommunityResolver {
|
|||||||
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.COMMUNITY_BY_UUID])
|
@Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER])
|
||||||
@Query(() => Community)
|
@Query(() => Community)
|
||||||
async community(@Arg('communityUuid') communityUuid: string): Promise<Community> {
|
async communityByIdentifier(
|
||||||
const com: DbCommunity | null = await getCommunityByUuid(communityUuid)
|
@Arg('communityIdentifier') communityIdentifier: string,
|
||||||
if (!com) {
|
): Promise<Community> {
|
||||||
throw new LogError('community not found', communityUuid)
|
const community = await getCommunityByIdentifier(communityIdentifier)
|
||||||
|
if (!community) {
|
||||||
|
throw new LogError('community not found', communityIdentifier)
|
||||||
}
|
}
|
||||||
return new Community(com)
|
return new Community(community)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.HOME_COMMUNITY])
|
||||||
|
@Query(() => Community)
|
||||||
|
async homeCommunity(): Promise<Community> {
|
||||||
|
const community = await getHomeCommunity()
|
||||||
|
if (!community) {
|
||||||
|
throw new LogError('no home community exist')
|
||||||
|
}
|
||||||
|
return new Community(community)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.COMMUNITY_UPDATE])
|
@Authorized([RIGHTS.COMMUNITY_UPDATE])
|
||||||
@Mutation(() => Community)
|
@Mutation(() => Community)
|
||||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise<Community> {
|
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise<Community> {
|
||||||
let homeCom: DbCommunity | null
|
const homeCom = await getCommunityByUuid(uuid)
|
||||||
let com: Community
|
if (!homeCom) {
|
||||||
if (uuid) {
|
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
||||||
let toUpdate = false
|
|
||||||
homeCom = await getCommunityByUuid(uuid)
|
|
||||||
if (!homeCom) {
|
|
||||||
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
|
||||||
}
|
|
||||||
if (homeCom.foreign) {
|
|
||||||
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
|
||||||
}
|
|
||||||
if (homeCom.gmsApiKey !== gmsApiKey) {
|
|
||||||
homeCom.gmsApiKey = gmsApiKey
|
|
||||||
toUpdate = true
|
|
||||||
}
|
|
||||||
if (toUpdate) {
|
|
||||||
await DbCommunity.save(homeCom)
|
|
||||||
}
|
|
||||||
com = new Community(homeCom)
|
|
||||||
} else {
|
|
||||||
throw new LogError(`HomeCommunity without an uuid can't be modified!`)
|
|
||||||
}
|
}
|
||||||
return com
|
if (homeCom.foreign) {
|
||||||
|
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
||||||
|
}
|
||||||
|
if (homeCom.gmsApiKey !== gmsApiKey) {
|
||||||
|
homeCom.gmsApiKey = gmsApiKey
|
||||||
|
await DbCommunity.save(homeCom)
|
||||||
|
}
|
||||||
|
return new Community(homeCom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
|
|||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
|
||||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
|
||||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
|
|
||||||
@ -534,9 +532,6 @@ describe('send coins', () => {
|
|||||||
mutation: updateUserInfos,
|
mutation: updateUserInfos,
|
||||||
variables: {
|
variables: {
|
||||||
alias: 'bob',
|
alias: 'bob',
|
||||||
gmsAllowed: true,
|
|
||||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
|
||||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await mutate({
|
await mutate({
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate'
|
|||||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||||
|
|
||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities'
|
import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities'
|
||||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||||
import { getLastTransaction } from './util/getLastTransaction'
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
import { getTransactionList } from './util/getTransactionList'
|
import { getTransactionList } from './util/getTransactionList'
|
||||||
@ -452,7 +452,7 @@ export class TransactionResolver {
|
|||||||
if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) {
|
if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) {
|
||||||
throw new LogError('X-Community sendCoins disabled per configuration!')
|
throw new LogError('X-Community sendCoins disabled per configuration!')
|
||||||
}
|
}
|
||||||
const recipCom = await getCommunityByUuid(recipientCommunityIdentifier)
|
const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier)
|
||||||
logger.debug('recipient commuity: ', recipCom)
|
logger.debug('recipient commuity: ', recipCom)
|
||||||
if (recipCom === null) {
|
if (recipCom === null) {
|
||||||
throw new LogError(
|
throw new LogError(
|
||||||
|
|||||||
@ -1303,8 +1303,10 @@ describe('UserResolver', () => {
|
|||||||
mutation: updateUserInfos,
|
mutation: updateUserInfos,
|
||||||
variables: {
|
variables: {
|
||||||
gmsAllowed: false,
|
gmsAllowed: false,
|
||||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL,
|
gmsPublishName:
|
||||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE,
|
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL],
|
||||||
|
gmsPublishLocation:
|
||||||
|
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual([
|
await expect(User.find()).resolves.toEqual([
|
||||||
@ -1326,9 +1328,11 @@ describe('UserResolver', () => {
|
|||||||
mutation: updateUserInfos,
|
mutation: updateUserInfos,
|
||||||
variables: {
|
variables: {
|
||||||
gmsAllowed: true,
|
gmsAllowed: true,
|
||||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
gmsPublishName:
|
||||||
|
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS],
|
||||||
gmsLocation: loc,
|
gmsLocation: loc,
|
||||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
gmsPublishLocation:
|
||||||
|
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual([
|
await expect(User.find()).resolves.toEqual([
|
||||||
@ -2674,9 +2678,6 @@ describe('UserResolver', () => {
|
|||||||
mutation: updateUserInfos,
|
mutation: updateUserInfos,
|
||||||
variables: {
|
variables: {
|
||||||
alias: 'bibi',
|
alias: 'bibi',
|
||||||
gmsAllowed: true,
|
|
||||||
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
|
|
||||||
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -19,8 +19,6 @@ import { SearchUsersFilters } from '@arg/SearchUsersFilters'
|
|||||||
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
|
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
|
||||||
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
|
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
|
||||||
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
|
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
|
||||||
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
|
|
||||||
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
|
|
||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||||
@ -563,17 +561,6 @@ export class UserResolver {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`,
|
`updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`,
|
||||||
)
|
)
|
||||||
// check default arg settings
|
|
||||||
if (gmsAllowed === null || gmsAllowed === undefined) {
|
|
||||||
gmsAllowed = true
|
|
||||||
}
|
|
||||||
if (!gmsPublishName) {
|
|
||||||
gmsPublishName = GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS
|
|
||||||
}
|
|
||||||
if (!gmsPublishLocation) {
|
|
||||||
gmsPublishLocation = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const backupOriginalUser = user
|
const backupOriginalUser = user
|
||||||
|
|
||||||
@ -623,13 +610,18 @@ export class UserResolver {
|
|||||||
if (hideAmountGDT !== undefined) {
|
if (hideAmountGDT !== undefined) {
|
||||||
user.hideAmountGDT = hideAmountGDT
|
user.hideAmountGDT = hideAmountGDT
|
||||||
}
|
}
|
||||||
|
if (gmsAllowed !== undefined) {
|
||||||
user.gmsAllowed = gmsAllowed
|
user.gmsAllowed = gmsAllowed
|
||||||
user.gmsPublishName = gmsPublishName
|
}
|
||||||
|
if (gmsPublishName !== null && gmsPublishName !== undefined) {
|
||||||
|
user.gmsPublishName = gmsPublishName
|
||||||
|
}
|
||||||
if (gmsLocation) {
|
if (gmsLocation) {
|
||||||
user.location = Location2Point(gmsLocation)
|
user.location = Location2Point(gmsLocation)
|
||||||
}
|
}
|
||||||
user.gmsPublishLocation = gmsPublishLocation
|
if (gmsPublishLocation !== null && gmsPublishLocation !== undefined) {
|
||||||
|
user.gmsPublishLocation = gmsPublishLocation
|
||||||
|
}
|
||||||
// } catch (err) {
|
// } catch (err) {
|
||||||
// console.log('error:', err)
|
// console.log('error:', err)
|
||||||
// }
|
// }
|
||||||
|
|||||||
@ -1,65 +1,146 @@
|
|||||||
|
import { FindOneOptions } from '@dbTools/typeorm'
|
||||||
import { Community as DbCommunity } from '@entity/Community'
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
|
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||||
|
|
||||||
|
import { Paginated } from '@arg/Paginated'
|
||||||
|
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
import { Connection } from '@/typeorm/connection'
|
||||||
|
|
||||||
|
function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions<DbCommunity> {
|
||||||
|
return {
|
||||||
|
where: [
|
||||||
|
{ communityUuid: communityIdentifier },
|
||||||
|
{ name: communityIdentifier },
|
||||||
|
{ url: communityIdentifier },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a community with the given identifier exists and is not foreign.
|
||||||
|
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||||
|
* @returns A promise that resolves to true if a non-foreign community exists with the given identifier, otherwise false.
|
||||||
|
*/
|
||||||
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
|
export async function isHomeCommunity(communityIdentifier: string): Promise<boolean> {
|
||||||
const homeCommunity = await DbCommunity.findOne({
|
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||||
|
// It essentially converts any truthy value to true and any falsy value to false.
|
||||||
|
return !!(await DbCommunity.findOne({
|
||||||
where: [
|
where: [
|
||||||
{ foreign: false, communityUuid: communityIdentifier },
|
{ foreign: false, communityUuid: communityIdentifier },
|
||||||
{ foreign: false, name: communityIdentifier },
|
{ foreign: false, name: communityIdentifier },
|
||||||
{ foreign: false, url: communityIdentifier },
|
{ foreign: false, url: communityIdentifier },
|
||||||
],
|
],
|
||||||
})
|
}))
|
||||||
if (homeCommunity) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the home community, i.e., a community that is not foreign.
|
||||||
|
* @returns A promise that resolves to the home community, or throw if no home community was found
|
||||||
|
*/
|
||||||
export async function getHomeCommunity(): Promise<DbCommunity> {
|
export async function getHomeCommunity(): Promise<DbCommunity> {
|
||||||
return await DbCommunity.findOneOrFail({
|
return await DbCommunity.findOneOrFail({
|
||||||
where: [{ foreign: false }],
|
where: [{ foreign: false }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Check if it is needed, because currently it isn't used at all
|
||||||
|
* Retrieves the URL of the community with the given identifier.
|
||||||
|
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||||
|
* @returns A promise that resolves to the URL of the community or throw if no community with this identifier was found
|
||||||
|
*/
|
||||||
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
|
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
|
||||||
const community = await DbCommunity.findOneOrFail({
|
return (await DbCommunity.findOneOrFail(findWithCommunityIdentifier(communityIdentifier))).url
|
||||||
where: [
|
|
||||||
{ communityUuid: communityIdentifier },
|
|
||||||
{ name: communityIdentifier },
|
|
||||||
{ url: communityIdentifier },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
return community.url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Check if it is needed, because currently it isn't used at all
|
||||||
|
* Checks if a community with the given identifier exists and has an authenticatedAt property set.
|
||||||
|
* @param communityIdentifier The identifier (URL, UUID, or name) of the community.
|
||||||
|
* @returns A promise that resolves to true if a community with an authenticatedAt property exists with the given identifier,
|
||||||
|
* otherwise false.
|
||||||
|
*/
|
||||||
export async function isCommunityAuthenticated(communityIdentifier: string): Promise<boolean> {
|
export async function isCommunityAuthenticated(communityIdentifier: string): Promise<boolean> {
|
||||||
const community = await DbCommunity.findOne({
|
// The !! operator in JavaScript or TypeScript is a shorthand for converting a value to a boolean.
|
||||||
where: [
|
// It essentially converts any truthy value to true and any falsy value to false.
|
||||||
{ communityUuid: communityIdentifier },
|
return !!(await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier)))
|
||||||
{ name: communityIdentifier },
|
?.authenticatedAt
|
||||||
{ url: communityIdentifier },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
if (community?.authenticatedAt) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the name of the community with the given identifier.
|
||||||
|
* @param communityIdentifier The identifier (URL, UUID) of the community.
|
||||||
|
* @returns A promise that resolves to the name of the community. If the community does not exist or has no name,
|
||||||
|
* an empty string is returned.
|
||||||
|
*/
|
||||||
export async function getCommunityName(communityIdentifier: string): Promise<string> {
|
export async function getCommunityName(communityIdentifier: string): Promise<string> {
|
||||||
const community = await DbCommunity.findOne({
|
const community = await DbCommunity.findOne({
|
||||||
where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }],
|
where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }],
|
||||||
})
|
})
|
||||||
if (community?.name) {
|
|
||||||
return community.name
|
|
||||||
} else {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return community?.name ? community.name : ''
|
||||||
|
}
|
||||||
export async function getCommunityByUuid(communityUuid: string): Promise<DbCommunity | null> {
|
export async function getCommunityByUuid(communityUuid: string): Promise<DbCommunity | null> {
|
||||||
return await DbCommunity.findOne({
|
return await DbCommunity.findOne({
|
||||||
where: [{ communityUuid }],
|
where: [{ communityUuid }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCommunityByIdentifier(
|
||||||
|
communityIdentifier: string,
|
||||||
|
): Promise<DbCommunity | null> {
|
||||||
|
return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate RIGHT Join between Communities and Federated Communities
|
||||||
|
* select *
|
||||||
|
* Community as c
|
||||||
|
* RIGHT JOIN FederatedCommunity as f
|
||||||
|
* ON(c.public_key = f.public_key)
|
||||||
|
* Typeorm don't has right joins
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getAllCommunities({
|
||||||
|
pageSize = 25,
|
||||||
|
currentPage = 1,
|
||||||
|
}: Paginated): Promise<DbCommunity[]> {
|
||||||
|
const connection = await Connection.getInstance()
|
||||||
|
if (!connection) {
|
||||||
|
throw new LogError('Cannot connect to db')
|
||||||
|
}
|
||||||
|
// foreign: 'ASC',
|
||||||
|
// createdAt: 'DESC',
|
||||||
|
// lastAnnouncedAt: 'DESC',
|
||||||
|
const result = await connection
|
||||||
|
.getRepository(DbFederatedCommunity)
|
||||||
|
.createQueryBuilder('federatedCommunity')
|
||||||
|
.leftJoinAndSelect('federatedCommunity.community', 'community')
|
||||||
|
.orderBy('federatedCommunity.foreign', 'ASC')
|
||||||
|
.addOrderBy('federatedCommunity.createdAt', 'DESC')
|
||||||
|
.addOrderBy('federatedCommunity.lastAnnouncedAt', 'DESC')
|
||||||
|
.skip((currentPage - 1) * pageSize * 3)
|
||||||
|
.take(pageSize * 3)
|
||||||
|
.getManyAndCount()
|
||||||
|
const communityMap = new Map<string, DbCommunity>()
|
||||||
|
result[0].forEach((value: DbFederatedCommunity) => {
|
||||||
|
const publicKeyHex = value.publicKey.toString('hex')
|
||||||
|
if (!communityMap.has(value.publicKey.toString('hex'))) {
|
||||||
|
let community: DbCommunity = DbCommunity.create()
|
||||||
|
if (value.community) {
|
||||||
|
community = value.community
|
||||||
|
}
|
||||||
|
if (!community.federatedCommunities) {
|
||||||
|
community.federatedCommunities = []
|
||||||
|
}
|
||||||
|
communityMap.set(publicKeyHex, community)
|
||||||
|
}
|
||||||
|
const community = communityMap.get(publicKeyHex)
|
||||||
|
if (!community?.federatedCommunities) {
|
||||||
|
throw new LogError('missing community after set it into map', publicKeyHex)
|
||||||
|
}
|
||||||
|
community.federatedCommunities.push(value)
|
||||||
|
})
|
||||||
|
return Array.from(communityMap.values())
|
||||||
|
}
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm'
|
|||||||
import { Community } from '@entity/Community'
|
import { Community } from '@entity/Community'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||||
|
import { isURL } from 'class-validator'
|
||||||
import { validate, version } from 'uuid'
|
import { validate, version } from 'uuid'
|
||||||
|
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
|
import { isEMail, isUUID4 } from '@/util/validate'
|
||||||
|
|
||||||
import { VALID_ALIAS_REGEX } from './validateAlias'
|
import { VALID_ALIAS_REGEX } from './validateAlias'
|
||||||
|
|
||||||
@ -19,10 +21,11 @@ export const findUserByIdentifier = async (
|
|||||||
communityIdentifier: string,
|
communityIdentifier: string,
|
||||||
): Promise<DbUser> => {
|
): Promise<DbUser> => {
|
||||||
let user: DbUser | null
|
let user: DbUser | null
|
||||||
const communityWhere: FindOptionsWhere<Community> =
|
const communityWhere: FindOptionsWhere<Community> = isURL(communityIdentifier)
|
||||||
validate(communityIdentifier) && version(communityIdentifier) === 4
|
? { url: communityIdentifier }
|
||||||
? { communityUuid: communityIdentifier }
|
: isUUID4(communityIdentifier)
|
||||||
: { name: communityIdentifier }
|
? { communityUuid: communityIdentifier }
|
||||||
|
: { name: communityIdentifier }
|
||||||
|
|
||||||
if (validate(identifier) && version(identifier) === 4) {
|
if (validate(identifier) && version(identifier) === 4) {
|
||||||
user = await DbUser.findOne({
|
user = await DbUser.findOne({
|
||||||
@ -32,7 +35,7 @@ export const findUserByIdentifier = async (
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)
|
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)
|
||||||
}
|
}
|
||||||
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
|
} else if (isEMail(identifier)) {
|
||||||
const userContact = await DbUserContact.findOne({
|
const userContact = await DbUserContact.findOne({
|
||||||
where: {
|
where: {
|
||||||
email: identifier,
|
email: identifier,
|
||||||
|
|||||||
@ -35,9 +35,9 @@ export const updateUserInfos = gql`
|
|||||||
$hideAmountGDD: Boolean
|
$hideAmountGDD: Boolean
|
||||||
$hideAmountGDT: Boolean
|
$hideAmountGDT: Boolean
|
||||||
$gmsAllowed: Boolean
|
$gmsAllowed: Boolean
|
||||||
$gmsPublishName: Int
|
$gmsPublishName: GmsPublishNameType
|
||||||
$gmsLocation: Location
|
$gmsLocation: Location
|
||||||
$gmsPublishLocation: Int
|
$gmsPublishLocation: GmsPublishLocationType
|
||||||
) {
|
) {
|
||||||
updateUserInfos(
|
updateUserInfos(
|
||||||
firstName: $firstName
|
firstName: $firstName
|
||||||
|
|||||||
@ -134,9 +134,25 @@ export const communitiesQuery = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const getCommunityByUuidQuery = gql`
|
export const getCommunityByIdentifierQuery = gql`
|
||||||
query ($communityUuid: String!) {
|
query ($communityIdentifier: String!) {
|
||||||
community(communityUuid: $communityUuid) {
|
communityByIdentifier(communityIdentifier: $communityIdentifier) {
|
||||||
|
id
|
||||||
|
foreign
|
||||||
|
name
|
||||||
|
description
|
||||||
|
url
|
||||||
|
creationDate
|
||||||
|
uuid
|
||||||
|
authenticatedAt
|
||||||
|
gmsApiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const getHomeCommunityQuery = gql`
|
||||||
|
query {
|
||||||
|
homeCommunity {
|
||||||
id
|
id
|
||||||
foreign
|
foreign
|
||||||
name
|
name
|
||||||
@ -156,7 +172,8 @@ export const getCommunities = gql`
|
|||||||
id
|
id
|
||||||
foreign
|
foreign
|
||||||
publicKey
|
publicKey
|
||||||
url
|
endPoint
|
||||||
|
apiVersion
|
||||||
lastAnnouncedAt
|
lastAnnouncedAt
|
||||||
verifiedAt
|
verifiedAt
|
||||||
lastErrorAt
|
lastErrorAt
|
||||||
@ -166,6 +183,34 @@ export const getCommunities = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const allCommunities = gql`
|
||||||
|
query {
|
||||||
|
allCommunities {
|
||||||
|
foreign
|
||||||
|
url
|
||||||
|
publicKey
|
||||||
|
uuid
|
||||||
|
authenticatedAt
|
||||||
|
name
|
||||||
|
description
|
||||||
|
gmsApiKey
|
||||||
|
creationDate
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
federatedCommunities {
|
||||||
|
id
|
||||||
|
apiVersion
|
||||||
|
endPoint
|
||||||
|
lastAnnouncedAt
|
||||||
|
verifiedAt
|
||||||
|
lastErrorAt
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const queryTransactionLink = gql`
|
export const queryTransactionLink = gql`
|
||||||
query ($code: String!) {
|
query ($code: String!) {
|
||||||
queryTransactionLink(code: $code) {
|
queryTransactionLink(code: $code) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { Decimal } from 'decimal.js-light'
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import { validate, version } from 'uuid'
|
||||||
|
|
||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
|
|
||||||
@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUUID4(value: string): boolean {
|
||||||
|
return validate(value) && version(value) === 4
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEMail(value: string): boolean {
|
||||||
|
return /^.{2,}@.{2,}\..{2,}$/.exec(value) !== null
|
||||||
|
}
|
||||||
|
|
||||||
async function calculateBalance(
|
async function calculateBalance(
|
||||||
userId: number,
|
userId: number,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -42,4 +51,4 @@ async function calculateBalance(
|
|||||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||||
}
|
}
|
||||||
|
|
||||||
export { calculateBalance, isStringBoolean }
|
export { calculateBalance, isStringBoolean, isUUID4, isEMail }
|
||||||
|
|||||||
@ -49,6 +49,7 @@
|
|||||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"@arg/*": ["src/graphql/arg/*"],
|
"@arg/*": ["src/graphql/arg/*"],
|
||||||
|
"@input/*": ["src/graphql/input/*"],
|
||||||
"@dltConnector/*": ["src/apis/dltConnector/*"],
|
"@dltConnector/*": ["src/apis/dltConnector/*"],
|
||||||
"@enum/*": ["src/graphql/enum/*"],
|
"@enum/*": ["src/graphql/enum/*"],
|
||||||
"@model/*": ["src/graphql/model/*"],
|
"@model/*": ["src/graphql/model/*"],
|
||||||
|
|||||||
1
database/.nvmrc
Normal file
1
database/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
v18.7.0
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { FederatedCommunity } from '../FederatedCommunity'
|
||||||
|
import { User } from '../User'
|
||||||
|
|
||||||
|
@Entity('communities')
|
||||||
|
export class Community extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||||
|
foreign: boolean
|
||||||
|
|
||||||
|
@Column({ name: 'url', length: 255, nullable: false })
|
||||||
|
url: string
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, nullable: false })
|
||||||
|
publicKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
|
||||||
|
privateKey: Buffer | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'community_uuid',
|
||||||
|
type: 'char',
|
||||||
|
length: 36,
|
||||||
|
nullable: true,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
communityUuid: string | null
|
||||||
|
|
||||||
|
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
|
||||||
|
authenticatedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
|
||||||
|
name: string | null
|
||||||
|
|
||||||
|
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
|
||||||
|
description: string | null
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
|
||||||
|
creationDate: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null })
|
||||||
|
gmsApiKey: string | null
|
||||||
|
|
||||||
|
@CreateDateColumn({
|
||||||
|
name: 'created_at',
|
||||||
|
type: 'datetime',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({
|
||||||
|
name: 'updated_at',
|
||||||
|
type: 'datetime',
|
||||||
|
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@OneToMany(() => User, (user) => user.community)
|
||||||
|
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||||
|
users: User[]
|
||||||
|
|
||||||
|
@OneToMany(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.community)
|
||||||
|
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
|
||||||
|
federatedCommunities?: FederatedCommunity[]
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Community } from '../Community'
|
||||||
|
|
||||||
|
@Entity('federated_communities')
|
||||||
|
export class FederatedCommunity extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||||
|
foreign: boolean
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
publicKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'api_version', length: 10, nullable: false })
|
||||||
|
apiVersion: string
|
||||||
|
|
||||||
|
@Column({ name: 'end_point', length: 255, nullable: false })
|
||||||
|
endPoint: string
|
||||||
|
|
||||||
|
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
|
||||||
|
lastAnnouncedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
|
||||||
|
verifiedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
|
||||||
|
lastErrorAt: Date | null
|
||||||
|
|
||||||
|
@CreateDateColumn({
|
||||||
|
name: 'created_at',
|
||||||
|
type: 'datetime',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({
|
||||||
|
name: 'updated_at',
|
||||||
|
type: 'datetime',
|
||||||
|
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
updatedAt: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => Community, (community) => community.federatedCommunities)
|
||||||
|
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
|
||||||
|
community?: Community
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { Community } from './0082-introduce_gms_registration/Community'
|
export { Community } from './0083-join_community_federated_communities/Community'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { FederatedCommunity } from './0068-community_tables_public_key_length/FederatedCommunity'
|
export { FederatedCommunity } from './0083-join_community_federated_communities/FederatedCommunity'
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {}
|
||||||
@ -26,7 +26,7 @@ EMAIL_CODE_REQUEST_TIME=10
|
|||||||
# config versions
|
# config versions
|
||||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||||
BACKEND_CONFIG_VERSION=v21.2024-01-06
|
BACKEND_CONFIG_VERSION=v21.2024-01-06
|
||||||
FRONTEND_CONFIG_VERSION=v5.2024-01-08
|
FRONTEND_CONFIG_VERSION=v6.2024-02-27
|
||||||
ADMIN_CONFIG_VERSION=v2.2024-01-04
|
ADMIN_CONFIG_VERSION=v2.2024-01-04
|
||||||
FEDERATION_CONFIG_VERSION=v2.2023-08-24
|
FEDERATION_CONFIG_VERSION=v2.2023-08-24
|
||||||
FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17
|
FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17
|
||||||
|
|||||||
@ -14,8 +14,8 @@ server {
|
|||||||
server {
|
server {
|
||||||
server_name $COMMUNITY_HOST;
|
server_name $COMMUNITY_HOST;
|
||||||
|
|
||||||
listen [::]:443 ssl ipv6only=on;
|
listen [::]:443 ssl ipv6only=on http2;
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
ssl_certificate $NGINX_SSL_CERTIFICATE;
|
ssl_certificate $NGINX_SSL_CERTIFICATE;
|
||||||
ssl_certificate_key $NGINX_SSL_CERTIFICATE_KEY;
|
ssl_certificate_key $NGINX_SSL_CERTIFICATE_KEY;
|
||||||
include $NGINX_SSL_INCLUDE;
|
include $NGINX_SSL_INCLUDE;
|
||||||
@ -33,7 +33,7 @@ server {
|
|||||||
return 444;
|
return 444;
|
||||||
}
|
}
|
||||||
|
|
||||||
#gzip_static on;
|
gzip_static on;
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_types
|
gzip_types
|
||||||
@ -53,18 +53,13 @@ server {
|
|||||||
|
|
||||||
# Frontend (default)
|
# Frontend (default)
|
||||||
location / {
|
location / {
|
||||||
|
|
||||||
limit_req zone=frontend burst=40 nodelay;
|
limit_req zone=frontend burst=40 nodelay;
|
||||||
limit_conn addr 40;
|
limit_conn addr 40;
|
||||||
proxy_http_version 1.1;
|
root $PROJECT_ROOT/frontend/build/;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
index index.html;
|
||||||
proxy_set_header Connection 'upgrade';
|
try_files $uri $uri/ /index.html = 404;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_redirect off;
|
|
||||||
|
|
||||||
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
||||||
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
||||||
}
|
}
|
||||||
@ -119,15 +114,10 @@ server {
|
|||||||
location /admin {
|
location /admin {
|
||||||
limit_req zone=frontend burst=30 nodelay;
|
limit_req zone=frontend burst=30 nodelay;
|
||||||
limit_conn addr 40;
|
limit_conn addr 40;
|
||||||
proxy_http_version 1.1;
|
rewrite ^/admin/(.*)$ /$1 break;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
root $PROJECT_ROOT/admin/build/;
|
||||||
proxy_set_header Connection 'upgrade';
|
index index.html;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
try_files $uri $uri/ /index.html = 404;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8080/;
|
|
||||||
proxy_redirect off;
|
|
||||||
|
|
||||||
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
||||||
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ server {
|
|||||||
return 444;
|
return 444;
|
||||||
}
|
}
|
||||||
|
|
||||||
#gzip_static on;
|
gzip_static on;
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_types
|
gzip_types
|
||||||
@ -40,15 +40,9 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
limit_req zone=frontend burst=40 nodelay;
|
limit_req zone=frontend burst=40 nodelay;
|
||||||
limit_conn addr 40;
|
limit_conn addr 40;
|
||||||
proxy_http_version 1.1;
|
root $PROJECT_ROOT/frontend/build/;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
index index.html;
|
||||||
proxy_set_header Connection 'upgrade';
|
try_files $uri $uri/ /index.html = 404;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_redirect off;
|
|
||||||
|
|
||||||
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
|
||||||
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
|
||||||
@ -104,15 +98,10 @@ server {
|
|||||||
location /admin {
|
location /admin {
|
||||||
limit_req zone=frontend burst=30 nodelay;
|
limit_req zone=frontend burst=30 nodelay;
|
||||||
limit_conn addr 40;
|
limit_conn addr 40;
|
||||||
proxy_http_version 1.1;
|
rewrite ^/admin/(.*)$ /$1 break;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
root $PROJECT_ROOT/admin/build/;
|
||||||
proxy_set_header Connection 'upgrade';
|
index index.html;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
try_files $uri $uri/ /index.html = 404;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8080/;
|
|
||||||
proxy_redirect off;
|
|
||||||
|
|
||||||
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log;
|
||||||
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
|
||||||
|
|||||||
@ -241,8 +241,8 @@ export NODE_ENV=production
|
|||||||
|
|
||||||
# start after building all to use up less ressources
|
# start after building all to use up less ressources
|
||||||
pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||||
pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
#pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||||
pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
#pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||||
pm2 save
|
pm2 save
|
||||||
if [ ! -z $FEDERATION_DHT_TOPIC ]; then
|
if [ ! -z $FEDERATION_DHT_TOPIC ]; then
|
||||||
pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||||
|
|||||||
@ -104,6 +104,23 @@ ln -s $SCRIPT_PATH/nginx/common /etc/nginx/
|
|||||||
rmdir /etc/nginx/conf.d
|
rmdir /etc/nginx/conf.d
|
||||||
ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/
|
ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/
|
||||||
|
|
||||||
|
# Make nginx restart automatic
|
||||||
|
mkdir /etc/systemd/system/nginx.service.d
|
||||||
|
# Define the content to be put into the override.conf file
|
||||||
|
CONFIG_CONTENT="[Unit]
|
||||||
|
StartLimitIntervalSec=500
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s"
|
||||||
|
|
||||||
|
# Write the content to the override.conf file
|
||||||
|
echo "$CONFIG_CONTENT" | sudo tee /etc/systemd/system/nginx.service.d/override.conf >/dev/null
|
||||||
|
|
||||||
|
# Reload systemd to apply the changes
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
# setup https with certbot
|
# setup https with certbot
|
||||||
certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL
|
certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL
|
||||||
|
|
||||||
|
|||||||
1
dht-node/.nvmrc
Normal file
1
dht-node/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
v19.5.0
|
||||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
|||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0082-introduce_gms_registration',
|
DB_VERSION: '0083-join_community_federated_communities',
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
CONFIG_VERSION=v4.2023-09-12
|
CONFIG_VERSION=v6.2024-02-20
|
||||||
|
|
||||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||||
@ -19,4 +19,8 @@ DB_DATABASE_TEST=gradido_dlt_test
|
|||||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||||
|
|
||||||
# DLT-Connector
|
# DLT-Connector
|
||||||
DLT_CONNECTOR_PORT=6010
|
DLT_CONNECTOR_PORT=6010
|
||||||
|
|
||||||
|
# Route to Backend
|
||||||
|
BACKEND_SERVER_URL=http://localhost:4000
|
||||||
|
JWT_SECRET=secret123
|
||||||
@ -1,5 +1,7 @@
|
|||||||
CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION
|
CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION
|
||||||
|
|
||||||
|
JWT_SECRET=$JWT_SECRET
|
||||||
|
|
||||||
#IOTA
|
#IOTA
|
||||||
IOTA_API_URL=$IOTA_API_URL
|
IOTA_API_URL=$IOTA_API_URL
|
||||||
IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS
|
IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS
|
||||||
@ -15,4 +17,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST
|
|||||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||||
|
|
||||||
# DLT-Connector
|
# DLT-Connector
|
||||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||||
|
|
||||||
|
# Route to Backend
|
||||||
|
BACKEND_SERVER_URL=http://localhost:4000
|
||||||
1
dlt-connector/.nvmrc
Normal file
1
dlt-connector/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
v19.5.0
|
||||||
@ -6,7 +6,7 @@ module.exports = {
|
|||||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
lines: 66,
|
lines: 72,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||||
|
|||||||
@ -31,15 +31,18 @@
|
|||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"express-slow-down": "^2.0.1",
|
"express-slow-down": "^2.0.1",
|
||||||
"graphql": "^16.7.1",
|
"graphql": "^16.7.1",
|
||||||
|
"graphql-request": "^6.1.0",
|
||||||
"graphql-scalars": "^1.22.2",
|
"graphql-scalars": "^1.22.2",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
|
"jose": "^5.2.2",
|
||||||
"log4js": "^6.7.1",
|
"log4js": "^6.7.1",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"protobufjs": "^7.2.5",
|
"protobufjs": "^7.2.5",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sodium-native": "^4.0.4",
|
"sodium-native": "^4.0.4",
|
||||||
"tsconfig-paths": "^4.1.2",
|
"tsconfig-paths": "^4.1.2",
|
||||||
"type-graphql": "^2.0.0-beta.2"
|
"type-graphql": "^2.0.0-beta.2",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
|
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
|
||||||
|
|||||||
105
dlt-connector/src/client/BackendClient.ts
Normal file
105
dlt-connector/src/client/BackendClient.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { gql, GraphQLClient } from 'graphql-request'
|
||||||
|
import { SignJWT } from 'jose'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
|
const homeCommunity = gql`
|
||||||
|
query {
|
||||||
|
homeCommunity {
|
||||||
|
uuid
|
||||||
|
foreign
|
||||||
|
creationDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
interface Community {
|
||||||
|
homeCommunity: {
|
||||||
|
uuid: string
|
||||||
|
foreign: boolean
|
||||||
|
creationDate: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||||
|
// and ../federation/client/FederationClientFactory.ts
|
||||||
|
/**
|
||||||
|
* A Singleton class defines the `getInstance` method that lets clients access
|
||||||
|
* the unique singleton instance.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
|
export class BackendClient {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
private static instance: BackendClient
|
||||||
|
client: GraphQLClient
|
||||||
|
/**
|
||||||
|
* The Singleton's constructor should always be private to prevent direct
|
||||||
|
* construction calls with the `new` operator.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The static method that controls the access to the singleton instance.
|
||||||
|
*
|
||||||
|
* This implementation let you subclass the Singleton class while keeping
|
||||||
|
* just one instance of each subclass around.
|
||||||
|
*/
|
||||||
|
public static getInstance(): BackendClient | undefined {
|
||||||
|
if (!BackendClient.instance) {
|
||||||
|
BackendClient.instance = new BackendClient()
|
||||||
|
}
|
||||||
|
if (!BackendClient.instance.client) {
|
||||||
|
try {
|
||||||
|
BackendClient.instance.client = new GraphQLClient(CONFIG.BACKEND_SERVER_URL, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'GET',
|
||||||
|
jsonSerializer: {
|
||||||
|
parse: JSON.parse,
|
||||||
|
stringify: JSON.stringify,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("couldn't connect to backend: ", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BackendClient.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHomeCommunityDraft(): Promise<CommunityDraft> {
|
||||||
|
logger.info('check home community on backend')
|
||||||
|
const { data, errors } = await this.client.rawRequest<Community>(
|
||||||
|
homeCommunity,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
authorization: 'Bearer ' + (await this.createJWTToken()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (errors) {
|
||||||
|
throw new LogError('error getting home community from backend', errors)
|
||||||
|
}
|
||||||
|
const communityDraft = new CommunityDraft()
|
||||||
|
communityDraft.uuid = data.homeCommunity.uuid
|
||||||
|
communityDraft.foreign = data.homeCommunity.foreign
|
||||||
|
communityDraft.createdAt = data.homeCommunity.creationDate
|
||||||
|
return communityDraft
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createJWTToken(): Promise<string> {
|
||||||
|
const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
|
||||||
|
const token = await new SignJWT({ gradidoID: 'dlt-connector', 'urn:gradido:claim': true })
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setIssuer('urn:gradido:issuer')
|
||||||
|
.setAudience('urn:gradido:audience')
|
||||||
|
.setExpirationTime('1m')
|
||||||
|
.sign(secret)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,7 +50,7 @@ jest.mock('@iota/client', () => {
|
|||||||
|
|
||||||
describe('Iota Tests', () => {
|
describe('Iota Tests', () => {
|
||||||
it('test mocked sendDataMessage', async () => {
|
it('test mocked sendDataMessage', async () => {
|
||||||
const result = await sendMessage('Test Message')
|
const result = await sendMessage('Test Message', 'topic')
|
||||||
expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710')
|
expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import { ClientBuilder } from '@iota/client'
|
|||||||
import { MessageWrapper } from '@iota/client/lib/types'
|
import { MessageWrapper } from '@iota/client/lib/types'
|
||||||
|
|
||||||
import { CONFIG } from '@/config'
|
import { CONFIG } from '@/config'
|
||||||
|
|
||||||
const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build()
|
const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* send data message onto iota tangle
|
* send data message onto iota tangle
|
||||||
* use CONFIG.IOTA_COMMUNITY_ALIAS for index
|
|
||||||
* @param {string | Uint8Array} message - the message as utf based string, will be converted to hex automatically from @iota/client
|
* @param {string | Uint8Array} message - the message as utf based string, will be converted to hex automatically from @iota/client
|
||||||
|
* @param {string | Uint8Array} topic - the iota topic to which the message will be sended
|
||||||
* @return {Promise<MessageWrapper>} the iota message typed
|
* @return {Promise<MessageWrapper>} the iota message typed
|
||||||
*/
|
*/
|
||||||
function sendMessage(message: string | Uint8Array): Promise<MessageWrapper> {
|
function sendMessage(
|
||||||
return client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit()
|
message: string | Uint8Array,
|
||||||
|
topic: string | Uint8Array,
|
||||||
|
): Promise<MessageWrapper> {
|
||||||
|
return client.message().index(topic).data(message).submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -9,13 +9,14 @@ const constants = {
|
|||||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v4.2023-09-12',
|
EXPECTED: 'v6.2024-02-20',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = {
|
const server = {
|
||||||
PRODUCTION: process.env.NODE_ENV === 'production' ?? false,
|
PRODUCTION: process.env.NODE_ENV === 'production' ?? false,
|
||||||
|
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
@ -31,13 +32,17 @@ const database = {
|
|||||||
const iota = {
|
const iota = {
|
||||||
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
|
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
|
||||||
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2',
|
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2',
|
||||||
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED ?? null,
|
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED?.substring(0, 32) ?? null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const dltConnector = {
|
const dltConnector = {
|
||||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
|
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backendServer = {
|
||||||
|
BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000',
|
||||||
|
}
|
||||||
|
|
||||||
// Check config version
|
// Check config version
|
||||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
||||||
if (
|
if (
|
||||||
@ -56,4 +61,5 @@ export const CONFIG = {
|
|||||||
...database,
|
...database,
|
||||||
...iota,
|
...iota,
|
||||||
...dltConnector,
|
...dltConnector,
|
||||||
|
...backendServer,
|
||||||
}
|
}
|
||||||
|
|||||||
35
dlt-connector/src/data/Account.logic.ts
Normal file
35
dlt-connector/src/data/Account.logic.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Account } from '@entity/Account'
|
||||||
|
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
|
import { KeyPair } from './KeyPair'
|
||||||
|
import { UserLogic } from './User.logic'
|
||||||
|
|
||||||
|
export class AccountLogic {
|
||||||
|
// eslint-disable-next-line no-useless-constructor
|
||||||
|
public constructor(private self: Account) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calculate account key pair starting from community key pair => derive user key pair => derive account key pair
|
||||||
|
* @param communityKeyPair
|
||||||
|
*/
|
||||||
|
public calculateKeyPair(communityKeyPair: KeyPair): KeyPair {
|
||||||
|
if (!this.self.user) {
|
||||||
|
throw new LogError('missing user')
|
||||||
|
}
|
||||||
|
const userLogic = new UserLogic(this.self.user)
|
||||||
|
const accountKeyPair = userLogic
|
||||||
|
.calculateKeyPair(communityKeyPair)
|
||||||
|
.derive([this.self.derivationIndex])
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.self.derive2Pubkey &&
|
||||||
|
this.self.derive2Pubkey.compare(accountKeyPair.publicKey) !== 0
|
||||||
|
) {
|
||||||
|
throw new LogError(
|
||||||
|
'The freshly derived public key does not correspond to the stored public key',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return accountKeyPair
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { LogError } from '@/server/LogError'
|
|||||||
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
|
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
|
||||||
|
|
||||||
import { Mnemonic } from './Mnemonic'
|
import { Mnemonic } from './Mnemonic'
|
||||||
|
import { SignaturePair } from './proto/3_3/SignaturePair'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Managing Key Pair and also generate, sign and verify signature with it
|
* Class Managing Key Pair and also generate, sign and verify signature with it
|
||||||
@ -81,7 +82,7 @@ export class KeyPair {
|
|||||||
return sign(message, this.getExtendPrivateKey())
|
return sign(message, this.getExtendPrivateKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
public verify(message: Buffer, signature: Buffer): boolean {
|
public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean {
|
||||||
return verify(message, signature, this.getExtendPublicKey())
|
return verify(message, signature, pubKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,13 @@ import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
|
|||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
import { randombytes_buf } from 'sodium-native'
|
import { randombytes_buf } from 'sodium-native'
|
||||||
|
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
export class Mnemonic {
|
export class Mnemonic {
|
||||||
private _passphrase = ''
|
private _passphrase = ''
|
||||||
public constructor(seed?: Buffer | string) {
|
public constructor(seed?: Buffer | string) {
|
||||||
if (seed) {
|
if (seed) {
|
||||||
|
Mnemonic.validateSeed(seed)
|
||||||
this._passphrase = entropyToMnemonic(seed)
|
this._passphrase = entropyToMnemonic(seed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -22,4 +25,24 @@ export class Mnemonic {
|
|||||||
public get seed(): Buffer {
|
public get seed(): Buffer {
|
||||||
return mnemonicToSeedSync(this._passphrase)
|
return mnemonicToSeedSync(this._passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static validateSeed(seed: Buffer | string): void {
|
||||||
|
let seedBuffer: Buffer
|
||||||
|
if (!Buffer.isBuffer(seed)) {
|
||||||
|
seedBuffer = Buffer.from(seed, 'hex')
|
||||||
|
} else {
|
||||||
|
seedBuffer = seed
|
||||||
|
}
|
||||||
|
if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) {
|
||||||
|
throw new LogError(
|
||||||
|
'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic',
|
||||||
|
{
|
||||||
|
seedBufferHex: seedBuffer.toString('hex'),
|
||||||
|
toShort: seedBuffer.length < 16,
|
||||||
|
toLong: seedBuffer.length > 32,
|
||||||
|
powerOf4: seedBuffer.length % 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,10 @@ export class TransactionBuilder {
|
|||||||
return this.transaction.community
|
return this.transaction.community
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getOtherCommunity(): Community | undefined {
|
||||||
|
return this.transaction.otherCommunity
|
||||||
|
}
|
||||||
|
|
||||||
public setSigningAccount(signingAccount: Account): TransactionBuilder {
|
public setSigningAccount(signingAccount: Account): TransactionBuilder {
|
||||||
this.transaction.signingAccount = signingAccount
|
this.transaction.signingAccount = signingAccount
|
||||||
return this
|
return this
|
||||||
@ -103,22 +107,18 @@ export class TransactionBuilder {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setSenderCommunityFromSenderUser(
|
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||||
senderUser: UserIdentifier,
|
|
||||||
): Promise<TransactionBuilder> {
|
|
||||||
// get sender community
|
// get sender community
|
||||||
const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser)
|
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||||
if (!community) {
|
if (!community) {
|
||||||
throw new LogError("couldn't find community for transaction")
|
throw new LogError("couldn't find community for transaction")
|
||||||
}
|
}
|
||||||
return this.setCommunity(community)
|
return this.setCommunity(community)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setOtherCommunityFromRecipientUser(
|
public async setOtherCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||||
recipientUser: UserIdentifier,
|
|
||||||
): Promise<TransactionBuilder> {
|
|
||||||
// get recipient community
|
// get recipient community
|
||||||
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser)
|
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||||
return this.setOtherCommunity(otherCommunity)
|
return this.setOtherCommunity(otherCommunity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal file
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
import { Community } from '@entity/Community'
|
||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
|
||||||
|
import { CommunityRoot } from './proto/3_3/CommunityRoot'
|
||||||
|
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||||
|
import { GradidoCreation } from './proto/3_3/GradidoCreation'
|
||||||
|
import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer'
|
||||||
|
import { GradidoTransfer } from './proto/3_3/GradidoTransfer'
|
||||||
|
import { RegisterAddress } from './proto/3_3/RegisterAddress'
|
||||||
|
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||||
|
import { TransactionLogic } from './Transaction.logic'
|
||||||
|
|
||||||
|
let a: Transaction
|
||||||
|
let b: Transaction
|
||||||
|
|
||||||
|
describe('data/transaction.logic', () => {
|
||||||
|
describe('isBelongTogether', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const now = new Date()
|
||||||
|
let body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.transfer = new GradidoTransfer()
|
||||||
|
body.otherGroup = 'recipient group'
|
||||||
|
|
||||||
|
a = new Transaction()
|
||||||
|
a.community = new Community()
|
||||||
|
a.communityId = 1
|
||||||
|
a.otherCommunityId = 2
|
||||||
|
a.id = 1
|
||||||
|
a.signingAccountId = 1
|
||||||
|
a.recipientAccountId = 2
|
||||||
|
a.createdAt = now
|
||||||
|
a.amount = new Decimal('100')
|
||||||
|
a.signature = Buffer.alloc(64)
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
|
||||||
|
body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
body.transfer = new GradidoTransfer()
|
||||||
|
body.otherGroup = 'sending group'
|
||||||
|
|
||||||
|
b = new Transaction()
|
||||||
|
b.community = new Community()
|
||||||
|
b.communityId = 2
|
||||||
|
b.otherCommunityId = 1
|
||||||
|
b.id = 2
|
||||||
|
b.signingAccountId = 1
|
||||||
|
b.recipientAccountId = 2
|
||||||
|
b.createdAt = now
|
||||||
|
b.amount = new Decimal('100')
|
||||||
|
b.signature = Buffer.alloc(64)
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
})
|
||||||
|
|
||||||
|
const spy = jest.spyOn(logger, 'info')
|
||||||
|
|
||||||
|
it('true', () => {
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of same id', () => {
|
||||||
|
b.id = 1
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different signing accounts', () => {
|
||||||
|
b.signingAccountId = 17
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
'transaction a and b are not pairs',
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different recipient accounts', () => {
|
||||||
|
b.recipientAccountId = 21
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
'transaction a and b are not pairs',
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different community ids', () => {
|
||||||
|
b.communityId = 6
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
'transaction a and b are not pairs',
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different other community ids', () => {
|
||||||
|
b.otherCommunityId = 3
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
'transaction a and b are not pairs',
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different createdAt', () => {
|
||||||
|
b.createdAt = new Date('2021-01-01T17:12')
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
'transaction a and b are not pairs',
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('false because of mismatching cross group type', () => {
|
||||||
|
const body = new TransactionBody()
|
||||||
|
it('a is LOCAL', () => {
|
||||||
|
body.type = CrossGroupType.LOCAL
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL')
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('b is LOCAL', () => {
|
||||||
|
body.type = CrossGroupType.LOCAL
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL')
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('both are INBOUND', () => {
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('both are OUTBOUND', () => {
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a is CROSS', () => {
|
||||||
|
body.type = CrossGroupType.CROSS
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('b is CROSS', () => {
|
||||||
|
body.type = CrossGroupType.CROSS
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"cross group types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('true with a as INBOUND and b as OUTBOUND', () => {
|
||||||
|
let body = TransactionBody.fromBodyBytes(a.bodyBytes)
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
body = TransactionBody.fromBodyBytes(b.bodyBytes)
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('false because of transaction type not suitable for cross group transactions', () => {
|
||||||
|
const body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
it('without transaction type (broken TransactionBody)', () => {
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('not the same transaction types', () => {
|
||||||
|
const body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.registerAddress = new RegisterAddress()
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"transaction types don't match",
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('community root cannot be a cross group transaction', () => {
|
||||||
|
let body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.communityRoot = new CommunityRoot()
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
body.communityRoot = new CommunityRoot()
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Gradido Creation cannot be a cross group transaction', () => {
|
||||||
|
let body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.creation = new GradidoCreation()
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
body.creation = new GradidoCreation()
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Deferred Transfer cannot be a cross group transaction', () => {
|
||||||
|
let body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.INBOUND
|
||||||
|
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||||
|
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith(
|
||||||
|
"TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('false because of wrong amount', () => {
|
||||||
|
it('amount missing on a', () => {
|
||||||
|
a.amount = undefined
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('amount missing on b', () => {
|
||||||
|
b.amount = undefined
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('amount not the same', () => {
|
||||||
|
a.amount = new Decimal('101')
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because otherGroup are the same', () => {
|
||||||
|
const body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.transfer = new GradidoTransfer()
|
||||||
|
body.otherGroup = 'sending group'
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('false because of different memos', () => {
|
||||||
|
const body = new TransactionBody()
|
||||||
|
body.type = CrossGroupType.OUTBOUND
|
||||||
|
body.transfer = new GradidoTransfer()
|
||||||
|
body.otherGroup = 'recipient group'
|
||||||
|
body.memo = 'changed memo'
|
||||||
|
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||||
|
const logic = new TransactionLogic(a)
|
||||||
|
expect(logic.isBelongTogether(b)).toBe(false)
|
||||||
|
expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
200
dlt-connector/src/data/Transaction.logic.ts
Normal file
200
dlt-connector/src/data/Transaction.logic.ts
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
import { Not } from 'typeorm'
|
||||||
|
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view'
|
||||||
|
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
|
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||||
|
import { TransactionType } from './proto/3_3/enum/TransactionType'
|
||||||
|
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||||
|
|
||||||
|
export class TransactionLogic {
|
||||||
|
protected transactionBody: TransactionBody | undefined
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-useless-constructor
|
||||||
|
public constructor(private self: Transaction) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* search for transaction pair for Cross Group Transaction
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async findPairTransaction(): Promise<Transaction> {
|
||||||
|
const type = this.getBody().type
|
||||||
|
if (type === CrossGroupType.LOCAL) {
|
||||||
|
throw new LogError("local transaction don't has a pairing transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if already was loaded from db
|
||||||
|
if (this.self.pairingTransaction) {
|
||||||
|
return this.self.pairingTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.self.pairingTransaction) {
|
||||||
|
const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction })
|
||||||
|
if (pairingTransaction) {
|
||||||
|
return pairingTransaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if we find some in db
|
||||||
|
const sameCreationDateTransactions = await Transaction.findBy({
|
||||||
|
createdAt: this.self.createdAt,
|
||||||
|
id: Not(this.self.id),
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
sameCreationDateTransactions.length === 1 &&
|
||||||
|
this.isBelongTogether(sameCreationDateTransactions[0])
|
||||||
|
) {
|
||||||
|
return sameCreationDateTransactions[0]
|
||||||
|
}
|
||||||
|
// this approach only work if all entities get ids really incremented by one
|
||||||
|
if (type === CrossGroupType.OUTBOUND) {
|
||||||
|
const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 })
|
||||||
|
if (prevTransaction && this.isBelongTogether(prevTransaction)) {
|
||||||
|
return prevTransaction
|
||||||
|
}
|
||||||
|
} else if (type === CrossGroupType.INBOUND) {
|
||||||
|
const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 })
|
||||||
|
if (nextTransaction && this.isBelongTogether(nextTransaction)) {
|
||||||
|
return nextTransaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new LogError("couldn't find valid pairing transaction", {
|
||||||
|
id: this.self.id,
|
||||||
|
type: CrossGroupType[type],
|
||||||
|
transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if two transactions belong together
|
||||||
|
* are they pairs for a cross group transaction
|
||||||
|
* @param otherTransaction
|
||||||
|
*/
|
||||||
|
public isBelongTogether(otherTransaction: Transaction): boolean {
|
||||||
|
if (this.self.id === otherTransaction.id) {
|
||||||
|
logger.info('id is the same, it is the same transaction!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.self.signingAccountId !== otherTransaction.signingAccountId ||
|
||||||
|
this.self.recipientAccountId !== otherTransaction.recipientAccountId ||
|
||||||
|
this.self.communityId !== otherTransaction.otherCommunityId ||
|
||||||
|
this.self.otherCommunityId !== otherTransaction.communityId ||
|
||||||
|
this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime()
|
||||||
|
) {
|
||||||
|
logger.info('transaction a and b are not pairs', {
|
||||||
|
a: new TransactionLoggingView(this.self).toJSON(),
|
||||||
|
b: new TransactionLoggingView(otherTransaction).toJSON(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const body = this.getBody()
|
||||||
|
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
|
||||||
|
/**
|
||||||
|
* both must be Cross or
|
||||||
|
* one can be OUTBOUND and one can be INBOUND
|
||||||
|
* no one can be LOCAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!this.validCrossGroupTypes(body.type, otherBody.type)) {
|
||||||
|
logger.info("cross group types don't match", {
|
||||||
|
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||||
|
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const type = body.getTransactionType()
|
||||||
|
const otherType = otherBody.getTransactionType()
|
||||||
|
if (!type || !otherType) {
|
||||||
|
throw new LogError("couldn't determine transaction type", {
|
||||||
|
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||||
|
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (type !== otherType) {
|
||||||
|
logger.info("transaction types don't match", {
|
||||||
|
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||||
|
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
TransactionType.COMMUNITY_ROOT,
|
||||||
|
TransactionType.GRADIDO_CREATION,
|
||||||
|
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||||
|
].includes(type)
|
||||||
|
) {
|
||||||
|
logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
TransactionType.GRADIDO_CREATION,
|
||||||
|
TransactionType.GRADIDO_TRANSFER,
|
||||||
|
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||||
|
].includes(type)
|
||||||
|
) {
|
||||||
|
if (!this.self.amount || !otherTransaction.amount) {
|
||||||
|
logger.info('missing amount')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.self.amount.cmp(otherTransaction.amount.toString())) {
|
||||||
|
logger.info('amounts mismatch', {
|
||||||
|
a: this.self.amount.toString(),
|
||||||
|
b: otherTransaction.amount.toString(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.otherGroup === otherBody.otherGroup) {
|
||||||
|
logger.info('otherGroups are the same', {
|
||||||
|
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||||
|
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (body.memo !== otherBody.memo) {
|
||||||
|
logger.info('memo differ', {
|
||||||
|
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||||
|
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* both must be CROSS or
|
||||||
|
* one can be OUTBOUND and one can be INBOUND
|
||||||
|
* no one can be LOCAL
|
||||||
|
* @return true if crossGroupTypes are valid
|
||||||
|
*/
|
||||||
|
protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean {
|
||||||
|
logger.debug('compare ', {
|
||||||
|
a: CrossGroupType[a],
|
||||||
|
b: CrossGroupType[b],
|
||||||
|
})
|
||||||
|
if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) {
|
||||||
|
logger.info('no one can be LOCAL')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) ||
|
||||||
|
(a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND)
|
||||||
|
) {
|
||||||
|
return true // One can be INBOUND and one can be OUTBOUND
|
||||||
|
}
|
||||||
|
return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBody(): TransactionBody {
|
||||||
|
if (!this.transactionBody) {
|
||||||
|
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
|
||||||
|
}
|
||||||
|
return this.transactionBody
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ export class UserLogic {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param parentKeys if undefined use home community key pair
|
* @param parentKeys from home community for own user
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
1
dlt-connector/src/data/const.ts
Normal file
1
dlt-connector/src/data/const.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'
|
||||||
@ -1,8 +1,5 @@
|
|||||||
import { Field, Message } from 'protobufjs'
|
import { Field, Message } from 'protobufjs'
|
||||||
|
|
||||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
|
||||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
|
||||||
import { logger } from '@/logging/logger'
|
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
import { SignatureMap } from './SignatureMap'
|
import { SignatureMap } from './SignatureMap'
|
||||||
@ -46,14 +43,6 @@ export class GradidoTransaction extends Message<GradidoTransaction> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTransactionBody(): TransactionBody {
|
getTransactionBody(): TransactionBody {
|
||||||
try {
|
return TransactionBody.fromBodyBytes(this.bodyBytes)
|
||||||
return TransactionBody.decode(new Uint8Array(this.bodyBytes))
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('error decoding body from gradido transaction: %s', error)
|
|
||||||
throw new TransactionError(
|
|
||||||
TransactionErrorType.PROTO_DECODE_ERROR,
|
|
||||||
'cannot decode body from gradido transaction',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction } from '@entity/Transaction'
|
||||||
import { Field, Message, OneOf } from 'protobufjs'
|
import { Field, Message, OneOf } from 'protobufjs'
|
||||||
|
|
||||||
|
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||||
|
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
import { timestampToDate } from '@/utils/typeConverter'
|
import { timestampToDate } from '@/utils/typeConverter'
|
||||||
|
|
||||||
@ -36,6 +39,18 @@ export class TransactionBody extends Message<TransactionBody> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static fromBodyBytes(bodyBytes: Buffer) {
|
||||||
|
try {
|
||||||
|
return TransactionBody.decode(new Uint8Array(bodyBytes))
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('error decoding body from gradido transaction: %s', error)
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.PROTO_DECODE_ERROR,
|
||||||
|
'cannot decode body from gradido transaction',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Field.d(1, 'string')
|
@Field.d(1, 'string')
|
||||||
public memo: string
|
public memo: string
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,9 @@ export class UserIdentifier {
|
|||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
uuid: string
|
uuid: string
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String)
|
||||||
@IsUUID('4')
|
@IsUUID('4')
|
||||||
communityUuid?: string
|
communityUuid: string
|
||||||
|
|
||||||
@Field(() => Int, { defaultValue: 1, nullable: true })
|
@Field(() => Int, { defaultValue: 1, nullable: true })
|
||||||
@IsPositive()
|
@IsPositive()
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { Resolver, Arg, Mutation } from 'type-graphql'
|
|||||||
|
|
||||||
import { TransactionDraft } from '@input/TransactionDraft'
|
import { TransactionDraft } from '@input/TransactionDraft'
|
||||||
|
|
||||||
|
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||||
import { TransactionRepository } from '@/data/Transaction.repository'
|
import { TransactionRepository } from '@/data/Transaction.repository'
|
||||||
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
|
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
|
||||||
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
|
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
|
||||||
import { logger } from '@/logging/logger'
|
import { logger } from '@/logging/logger'
|
||||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||||
|
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||||
import { LogError } from '@/server/LogError'
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
import { TransactionError } from '../model/TransactionError'
|
import { TransactionError } from '../model/TransactionError'
|
||||||
@ -48,6 +50,7 @@ export class TransactionResolver {
|
|||||||
// we can store the transaction and with that automatic the backend transaction
|
// we can store the transaction and with that automatic the backend transaction
|
||||||
await transactionRecipe.save()
|
await transactionRecipe.save()
|
||||||
}
|
}
|
||||||
|
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||||
return new TransactionResult(new TransactionRecipe(transactionRecipe))
|
return new TransactionResult(new TransactionRecipe(transactionRecipe))
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -1,17 +1,77 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { CONFIG } from '@/config'
|
import { CONFIG } from '@/config'
|
||||||
|
|
||||||
|
import { BackendClient } from './client/BackendClient'
|
||||||
|
import { CommunityRepository } from './data/Community.repository'
|
||||||
|
import { Mnemonic } from './data/Mnemonic'
|
||||||
|
import { CommunityDraft } from './graphql/input/CommunityDraft'
|
||||||
|
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
|
||||||
|
import { logger } from './logging/logger'
|
||||||
import createServer from './server/createServer'
|
import createServer from './server/createServer'
|
||||||
|
import { LogError } from './server/LogError'
|
||||||
|
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
|
||||||
|
|
||||||
|
async function waitForServer(
|
||||||
|
backend: BackendClient,
|
||||||
|
retryIntervalMs: number,
|
||||||
|
maxRetries: number,
|
||||||
|
): Promise<CommunityDraft> {
|
||||||
|
let retries = 0
|
||||||
|
while (retries < maxRetries) {
|
||||||
|
logger.info(`Attempt ${retries + 1} for connecting to backend`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make a HEAD request to the server
|
||||||
|
return await backend.getHomeCommunityDraft()
|
||||||
|
} catch (error) {
|
||||||
|
logger.info('Server is not reachable: ', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is not reachable, wait and retry
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs))
|
||||||
|
retries++
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogError('Max retries exceeded. Server did not become reachable.')
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
|
||||||
|
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
|
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||||
const { app } = await createServer()
|
const { app } = await createServer()
|
||||||
|
|
||||||
|
// ask backend for home community if we haven't one
|
||||||
|
try {
|
||||||
|
await CommunityRepository.loadHomeCommunityKeyPair()
|
||||||
|
} catch (e) {
|
||||||
|
const backend = BackendClient.getInstance()
|
||||||
|
if (!backend) {
|
||||||
|
throw new LogError('cannot create backend client')
|
||||||
|
}
|
||||||
|
// wait for backend server to be ready
|
||||||
|
await waitForServer(backend, 2500, 10)
|
||||||
|
|
||||||
|
const communityDraft = await backend.getHomeCommunityDraft()
|
||||||
|
const addCommunityContext = new AddCommunityContext(communityDraft)
|
||||||
|
await addCommunityContext.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop run all the time, check for new transaction for sending to iota
|
||||||
|
void transmitToIota()
|
||||||
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
|
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`)
|
console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
// Add shutdown logic here.
|
||||||
|
stopTransmitToIota()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
import { Community } from '@entity/Community'
|
||||||
|
|
||||||
|
import { TestDB } from '@test/TestDB'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||||
|
|
||||||
|
import { AddCommunityContext } from './AddCommunity.context'
|
||||||
|
|
||||||
|
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||||
|
|
||||||
|
jest.mock('@typeorm/DataSource', () => ({
|
||||||
|
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('interactions/backendToDb/community/AddCommunity Context Test', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await TestDB.instance.setupTestDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await TestDB.instance.teardownTestDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
const homeCommunityDraft = new CommunityDraft()
|
||||||
|
homeCommunityDraft.uuid = 'a2fd0fee-f3ba-4bef-a62a-10a34b0e2754'
|
||||||
|
homeCommunityDraft.foreign = false
|
||||||
|
homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z'
|
||||||
|
// calculated from a2fd0fee-f3ba-4bef-a62a-10a34b0e2754 with iotaTopicFromCommunityUUID
|
||||||
|
const iotaTopic = '7be2ad83f279a3aaf6d62371cb6be301e2e3c7a3efda9c89984e8f6a7865d9ce'
|
||||||
|
|
||||||
|
const foreignCommunityDraft = new CommunityDraft()
|
||||||
|
foreignCommunityDraft.uuid = '70df8de5-0fb7-4153-a124-4ff86965be9a'
|
||||||
|
foreignCommunityDraft.foreign = true
|
||||||
|
foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z'
|
||||||
|
|
||||||
|
it('with home community, without iota topic', async () => {
|
||||||
|
const context = new AddCommunityContext(homeCommunityDraft)
|
||||||
|
await context.run()
|
||||||
|
const homeCommunity = await Community.findOneOrFail({ where: { iotaTopic } })
|
||||||
|
expect(homeCommunity).toMatchObject({
|
||||||
|
id: 1,
|
||||||
|
iotaTopic,
|
||||||
|
foreign: 0,
|
||||||
|
rootPubkey: Buffer.from(
|
||||||
|
'07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0',
|
||||||
|
'hex',
|
||||||
|
),
|
||||||
|
createdAt: new Date('2024-01-25T13:09:55.339Z'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('with foreign community', async () => {
|
||||||
|
const context = new AddCommunityContext(foreignCommunityDraft, 'randomTopic')
|
||||||
|
await context.run()
|
||||||
|
const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } })
|
||||||
|
expect(foreignCommunity).toMatchObject({
|
||||||
|
id: 2,
|
||||||
|
iotaTopic: 'randomTopic',
|
||||||
|
foreign: 1,
|
||||||
|
createdAt: new Date('2024-01-25T13:34:28.020Z'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -3,6 +3,7 @@ import { Transaction } from '@entity/Transaction'
|
|||||||
|
|
||||||
import { CONFIG } from '@/config'
|
import { CONFIG } from '@/config'
|
||||||
import { AccountFactory } from '@/data/Account.factory'
|
import { AccountFactory } from '@/data/Account.factory'
|
||||||
|
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||||
import { KeyPair } from '@/data/KeyPair'
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
import { Mnemonic } from '@/data/Mnemonic'
|
import { Mnemonic } from '@/data/Mnemonic'
|
||||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||||
@ -10,6 +11,8 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
|||||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||||
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
|
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
|
||||||
import { logger } from '@/logging/logger'
|
import { logger } from '@/logging/logger'
|
||||||
|
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
import { getDataSource } from '@/typeorm/DataSource'
|
import { getDataSource } from '@/typeorm/DataSource'
|
||||||
|
|
||||||
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
||||||
@ -22,7 +25,19 @@ export class HomeCommunityRole extends CommunityRole {
|
|||||||
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
||||||
super.create(communityDraft, topic)
|
super.create(communityDraft, topic)
|
||||||
// generate key pair for signing transactions and deriving all keys for community
|
// generate key pair for signing transactions and deriving all keys for community
|
||||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined))
|
let mnemonic: Mnemonic
|
||||||
|
try {
|
||||||
|
mnemonic = new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)
|
||||||
|
} catch (e) {
|
||||||
|
throw new LogError(
|
||||||
|
'error creating mnemonic for home community, please fill IOTA_HOME_COMMUNITY_SEED in .env',
|
||||||
|
{
|
||||||
|
IOTA_HOME_COMMUNITY_SEED: CONFIG.IOTA_HOME_COMMUNITY_SEED,
|
||||||
|
error: e,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const keyPair = new KeyPair(mnemonic)
|
||||||
keyPair.fillInCommunityKeys(this.self)
|
keyPair.fillInCommunityKeys(this.self)
|
||||||
|
|
||||||
// create auf account and gmw account
|
// create auf account and gmw account
|
||||||
@ -36,12 +51,14 @@ export class HomeCommunityRole extends CommunityRole {
|
|||||||
|
|
||||||
public async store(): Promise<Community> {
|
public async store(): Promise<Community> {
|
||||||
try {
|
try {
|
||||||
return await getDataSource().transaction(async (transactionalEntityManager) => {
|
const community = await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||||
const community = await transactionalEntityManager.save(this.self)
|
const community = await transactionalEntityManager.save(this.self)
|
||||||
await transactionalEntityManager.save(this.transactionRecipe)
|
await transactionalEntityManager.save(this.transactionRecipe)
|
||||||
logger.debug('store home community', new CommunityLoggingView(community))
|
logger.debug('store home community', new CommunityLoggingView(community))
|
||||||
return community
|
return community
|
||||||
})
|
})
|
||||||
|
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||||
|
return community
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('error saving home community into db: %s', error)
|
logger.error('error saving home community into db: %s', error)
|
||||||
throw new TransactionError(
|
throw new TransactionError(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
|||||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||||
|
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||||
|
|
||||||
export abstract class AbstractTransactionRole {
|
export abstract class AbstractTransactionRole {
|
||||||
// eslint-disable-next-line no-useless-constructor
|
// eslint-disable-next-line no-useless-constructor
|
||||||
@ -26,7 +27,7 @@ export abstract class AbstractTransactionRole {
|
|||||||
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
|
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
|
||||||
* INBOUND: goes to receiver, stored on receiver community blockchain
|
* INBOUND: goes to receiver, stored on receiver community blockchain
|
||||||
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
|
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
|
||||||
* @returns
|
* @returns iota topic
|
||||||
*/
|
*/
|
||||||
public getOtherGroup(): string {
|
public getOtherGroup(): string {
|
||||||
let user: UserIdentifier
|
let user: UserIdentifier
|
||||||
@ -42,7 +43,7 @@ export abstract class AbstractTransactionRole {
|
|||||||
'missing sender/signing user community id for cross group transaction',
|
'missing sender/signing user community id for cross group transaction',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return user.communityUuid
|
return iotaTopicFromCommunityUUID(user.communityUuid)
|
||||||
case CrossGroupType.OUTBOUND:
|
case CrossGroupType.OUTBOUND:
|
||||||
user = this.getRecipientUser()
|
user = this.getRecipientUser()
|
||||||
if (!user.communityUuid) {
|
if (!user.communityUuid) {
|
||||||
@ -51,7 +52,7 @@ export abstract class AbstractTransactionRole {
|
|||||||
'missing recipient user community id for cross group transaction',
|
'missing recipient user community id for cross group transaction',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return user.communityUuid
|
return iotaTopicFromCommunityUUID(user.communityUuid)
|
||||||
default:
|
default:
|
||||||
throw new TransactionError(
|
throw new TransactionError(
|
||||||
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
||||||
|
|||||||
@ -0,0 +1,340 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
import { Account } from '@entity/Account'
|
||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
import { TestDB } from '@test/TestDB'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { Mnemonic } from '@/data/Mnemonic'
|
||||||
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
|
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
|
||||||
|
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||||
|
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||||
|
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||||
|
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||||
|
|
||||||
|
import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import { communitySeed } from '@test/seeding/Community.seed'
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
|
||||||
|
|
||||||
|
jest.mock('@typeorm/DataSource', () => ({
|
||||||
|
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||||
|
}))
|
||||||
|
|
||||||
|
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||||
|
const homeCommunityUuid = v4()
|
||||||
|
const foreignCommunityUuid = v4()
|
||||||
|
|
||||||
|
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
|
||||||
|
const foreignKeyPair = new KeyPair(
|
||||||
|
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
|
||||||
|
)
|
||||||
|
|
||||||
|
let moderator: UserSet
|
||||||
|
let firstUser: UserSet
|
||||||
|
let secondUser: UserSet
|
||||||
|
let foreignUser: UserSet
|
||||||
|
|
||||||
|
const topic = iotaTopicFromCommunityUUID(homeCommunityUuid)
|
||||||
|
const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid)
|
||||||
|
|
||||||
|
describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await TestDB.instance.setupTestDB()
|
||||||
|
await communitySeed(homeCommunityUuid, false)
|
||||||
|
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
|
||||||
|
|
||||||
|
moderator = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
firstUser = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
secondUser = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
|
||||||
|
|
||||||
|
await Account.save([
|
||||||
|
moderator.account,
|
||||||
|
firstUser.account,
|
||||||
|
secondUser.account,
|
||||||
|
foreignUser.account,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await TestDB.instance.teardownTestDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creation transaction', async () => {
|
||||||
|
const creationTransactionDraft = new TransactionDraft()
|
||||||
|
creationTransactionDraft.amount = new Decimal('2000')
|
||||||
|
creationTransactionDraft.backendTransactionId = 1
|
||||||
|
creationTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
creationTransactionDraft.linkedUser = moderator.identifier
|
||||||
|
creationTransactionDraft.user = firstUser.identifier
|
||||||
|
creationTransactionDraft.type = InputTransactionType.CREATION
|
||||||
|
creationTransactionDraft.targetDate = new Date().toISOString()
|
||||||
|
const context = new CreateTransactionRecipeContext(creationTransactionDraft)
|
||||||
|
await context.run()
|
||||||
|
const transaction = context.getTransactionRecipe()
|
||||||
|
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
expect(transaction).toMatchObject({
|
||||||
|
type: TransactionType.GRADIDO_CREATION,
|
||||||
|
protocolVersion: '3.3',
|
||||||
|
community: {
|
||||||
|
rootPubkey: keyPair.publicKey,
|
||||||
|
foreign: 0,
|
||||||
|
iotaTopic: topic,
|
||||||
|
},
|
||||||
|
signingAccount: {
|
||||||
|
derive2Pubkey: moderator.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
recipientAccount: {
|
||||||
|
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
amount: new Decimal(2000),
|
||||||
|
backendTransactions: [
|
||||||
|
{
|
||||||
|
typeId: InputTransactionType.CREATION,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
// console.log(new TransactionBodyLoggingView(body))
|
||||||
|
expect(body.creation).toBeDefined()
|
||||||
|
if (!body.creation) throw new Error()
|
||||||
|
const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey)
|
||||||
|
expect(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0)
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
type: CrossGroupType.LOCAL,
|
||||||
|
creation: {
|
||||||
|
recipient: {
|
||||||
|
amount: '2000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('local send transaction', async () => {
|
||||||
|
const sendTransactionDraft = new TransactionDraft()
|
||||||
|
sendTransactionDraft.amount = new Decimal('100')
|
||||||
|
sendTransactionDraft.backendTransactionId = 2
|
||||||
|
sendTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
sendTransactionDraft.linkedUser = secondUser.identifier
|
||||||
|
sendTransactionDraft.user = firstUser.identifier
|
||||||
|
sendTransactionDraft.type = InputTransactionType.SEND
|
||||||
|
const context = new CreateTransactionRecipeContext(sendTransactionDraft)
|
||||||
|
await context.run()
|
||||||
|
const transaction = context.getTransactionRecipe()
|
||||||
|
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
expect(transaction).toMatchObject({
|
||||||
|
type: TransactionType.GRADIDO_TRANSFER,
|
||||||
|
protocolVersion: '3.3',
|
||||||
|
community: {
|
||||||
|
rootPubkey: keyPair.publicKey,
|
||||||
|
foreign: 0,
|
||||||
|
iotaTopic: topic,
|
||||||
|
},
|
||||||
|
signingAccount: {
|
||||||
|
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
recipientAccount: {
|
||||||
|
derive2Pubkey: secondUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
amount: new Decimal(100),
|
||||||
|
backendTransactions: [
|
||||||
|
{
|
||||||
|
typeId: InputTransactionType.SEND,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
// console.log(new TransactionBodyLoggingView(body))
|
||||||
|
expect(body.transfer).toBeDefined()
|
||||||
|
if (!body.transfer) throw new Error()
|
||||||
|
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
|
||||||
|
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
type: CrossGroupType.LOCAL,
|
||||||
|
transfer: {
|
||||||
|
sender: {
|
||||||
|
amount: '100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('local recv transaction', async () => {
|
||||||
|
const recvTransactionDraft = new TransactionDraft()
|
||||||
|
recvTransactionDraft.amount = new Decimal('100')
|
||||||
|
recvTransactionDraft.backendTransactionId = 3
|
||||||
|
recvTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
recvTransactionDraft.linkedUser = firstUser.identifier
|
||||||
|
recvTransactionDraft.user = secondUser.identifier
|
||||||
|
recvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||||
|
const context = new CreateTransactionRecipeContext(recvTransactionDraft)
|
||||||
|
await context.run()
|
||||||
|
const transaction = context.getTransactionRecipe()
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
expect(transaction).toMatchObject({
|
||||||
|
type: TransactionType.GRADIDO_TRANSFER,
|
||||||
|
protocolVersion: '3.3',
|
||||||
|
community: {
|
||||||
|
rootPubkey: keyPair.publicKey,
|
||||||
|
foreign: 0,
|
||||||
|
iotaTopic: topic,
|
||||||
|
},
|
||||||
|
signingAccount: {
|
||||||
|
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
recipientAccount: {
|
||||||
|
derive2Pubkey: secondUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
amount: new Decimal(100),
|
||||||
|
backendTransactions: [
|
||||||
|
{
|
||||||
|
typeId: InputTransactionType.RECEIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
// console.log(new TransactionBodyLoggingView(body))
|
||||||
|
expect(body.transfer).toBeDefined()
|
||||||
|
if (!body.transfer) throw new Error()
|
||||||
|
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
|
||||||
|
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
type: CrossGroupType.LOCAL,
|
||||||
|
transfer: {
|
||||||
|
sender: {
|
||||||
|
amount: '100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cross group send transaction', async () => {
|
||||||
|
const crossGroupSendTransactionDraft = new TransactionDraft()
|
||||||
|
crossGroupSendTransactionDraft.amount = new Decimal('100')
|
||||||
|
crossGroupSendTransactionDraft.backendTransactionId = 4
|
||||||
|
crossGroupSendTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
|
||||||
|
crossGroupSendTransactionDraft.user = firstUser.identifier
|
||||||
|
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
|
||||||
|
const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft)
|
||||||
|
await context.run()
|
||||||
|
const transaction = context.getTransactionRecipe()
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
expect(transaction).toMatchObject({
|
||||||
|
type: TransactionType.GRADIDO_TRANSFER,
|
||||||
|
protocolVersion: '3.3',
|
||||||
|
community: {
|
||||||
|
rootPubkey: keyPair.publicKey,
|
||||||
|
foreign: 0,
|
||||||
|
iotaTopic: topic,
|
||||||
|
},
|
||||||
|
otherCommunity: {
|
||||||
|
rootPubkey: foreignKeyPair.publicKey,
|
||||||
|
foreign: 1,
|
||||||
|
iotaTopic: foreignTopic,
|
||||||
|
},
|
||||||
|
signingAccount: {
|
||||||
|
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
recipientAccount: {
|
||||||
|
derive2Pubkey: foreignUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
amount: new Decimal(100),
|
||||||
|
backendTransactions: [
|
||||||
|
{
|
||||||
|
typeId: InputTransactionType.SEND,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
// console.log(new TransactionBodyLoggingView(body))
|
||||||
|
expect(body.transfer).toBeDefined()
|
||||||
|
if (!body.transfer) throw new Error()
|
||||||
|
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
|
||||||
|
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
type: CrossGroupType.OUTBOUND,
|
||||||
|
otherGroup: foreignTopic,
|
||||||
|
transfer: {
|
||||||
|
sender: {
|
||||||
|
amount: '100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cross group recv transaction', async () => {
|
||||||
|
const crossGroupRecvTransactionDraft = new TransactionDraft()
|
||||||
|
crossGroupRecvTransactionDraft.amount = new Decimal('100')
|
||||||
|
crossGroupRecvTransactionDraft.backendTransactionId = 5
|
||||||
|
crossGroupRecvTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
|
||||||
|
crossGroupRecvTransactionDraft.user = foreignUser.identifier
|
||||||
|
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||||
|
const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft)
|
||||||
|
await context.run()
|
||||||
|
const transaction = context.getTransactionRecipe()
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
expect(transaction).toMatchObject({
|
||||||
|
type: TransactionType.GRADIDO_TRANSFER,
|
||||||
|
protocolVersion: '3.3',
|
||||||
|
community: {
|
||||||
|
rootPubkey: foreignKeyPair.publicKey,
|
||||||
|
foreign: 1,
|
||||||
|
iotaTopic: foreignTopic,
|
||||||
|
},
|
||||||
|
otherCommunity: {
|
||||||
|
rootPubkey: keyPair.publicKey,
|
||||||
|
foreign: 0,
|
||||||
|
iotaTopic: topic,
|
||||||
|
},
|
||||||
|
signingAccount: {
|
||||||
|
derive2Pubkey: firstUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
recipientAccount: {
|
||||||
|
derive2Pubkey: foreignUser.account.derive2Pubkey,
|
||||||
|
},
|
||||||
|
amount: new Decimal(100),
|
||||||
|
backendTransactions: [
|
||||||
|
{
|
||||||
|
typeId: InputTransactionType.RECEIVE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
// console.log(new TransactionBodyLoggingView(body))
|
||||||
|
expect(body.transfer).toBeDefined()
|
||||||
|
if (!body.transfer) throw new Error()
|
||||||
|
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
|
||||||
|
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
type: CrossGroupType.INBOUND,
|
||||||
|
otherGroup: topic,
|
||||||
|
transfer: {
|
||||||
|
sender: {
|
||||||
|
amount: '100',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,12 @@
|
|||||||
|
import { Community } from '@entity/Community'
|
||||||
|
|
||||||
|
import { CommunityRepository } from '@/data/Community.repository'
|
||||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
|
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||||
|
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { UserIdentifierLoggingView } from '@/logging/UserIdentifierLogging.view'
|
||||||
|
|
||||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||||
|
|
||||||
@ -15,4 +22,26 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
|||||||
public getCrossGroupType(): CrossGroupType {
|
public getCrossGroupType(): CrossGroupType {
|
||||||
return CrossGroupType.LOCAL
|
return CrossGroupType.LOCAL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCommunity(): Promise<Community> {
|
||||||
|
if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) {
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.LOGIC_ERROR,
|
||||||
|
'mismatch community uuids on creation transaction',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const community = await CommunityRepository.getCommunityForUserIdentifier(this.self.user)
|
||||||
|
if (!community) {
|
||||||
|
logger.error(
|
||||||
|
'missing community for user identifier',
|
||||||
|
new UserIdentifierLoggingView(this.self.user),
|
||||||
|
)
|
||||||
|
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't find community for user")
|
||||||
|
}
|
||||||
|
return community
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getOtherCommunity(): Promise<Community | null> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import { Community } from '@entity/Community'
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
|
import { AccountLogic } from '@/data/Account.logic'
|
||||||
import { KeyPair } from '@/data/KeyPair'
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
|
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
|
||||||
import { TransactionBuilder } from '@/data/Transaction.builder'
|
import { TransactionBuilder } from '@/data/Transaction.builder'
|
||||||
import { UserRepository } from '@/data/User.repository'
|
import { UserRepository } from '@/data/User.repository'
|
||||||
@ -52,18 +55,34 @@ export class TransactionRecipeRole {
|
|||||||
this.transactionBuilder
|
this.transactionBuilder
|
||||||
.fromTransactionBodyBuilder(transactionBodyBuilder)
|
.fromTransactionBodyBuilder(transactionBodyBuilder)
|
||||||
.addBackendTransaction(transactionDraft)
|
.addBackendTransaction(transactionDraft)
|
||||||
await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser)
|
|
||||||
|
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
|
||||||
if (recipientUser.communityUuid !== signingUser.communityUuid) {
|
if (recipientUser.communityUuid !== signingUser.communityUuid) {
|
||||||
await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser)
|
await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser)
|
||||||
}
|
}
|
||||||
const transaction = this.transactionBuilder.getTransaction()
|
const transaction = this.transactionBuilder.getTransaction()
|
||||||
|
const communityKeyPair = new KeyPair(
|
||||||
|
this.getSigningCommunity(transactionTypeRole.getCrossGroupType()),
|
||||||
|
)
|
||||||
|
const accountLogic = new AccountLogic(signingAccount)
|
||||||
// sign
|
// sign
|
||||||
this.transactionBuilder.setSignature(
|
this.transactionBuilder.setSignature(
|
||||||
new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes),
|
accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes),
|
||||||
)
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSigningCommunity(crossGroupType: CrossGroupType): Community {
|
||||||
|
if (crossGroupType === CrossGroupType.INBOUND) {
|
||||||
|
const otherCommunity = this.transactionBuilder.getOtherCommunity()
|
||||||
|
if (!otherCommunity) {
|
||||||
|
throw new TransactionError(TransactionErrorType.NOT_FOUND, 'missing other community')
|
||||||
|
}
|
||||||
|
return otherCommunity
|
||||||
|
}
|
||||||
|
return this.transactionBuilder.getCommunity()
|
||||||
|
}
|
||||||
|
|
||||||
public getTransaction(): Transaction {
|
public getTransaction(): Transaction {
|
||||||
return this.transactionBuilder.getTransaction()
|
return this.transactionBuilder.getTransaction()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
|
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
|
||||||
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
|
||||||
|
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
|
||||||
|
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||||
|
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||||
|
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||||
|
import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLogging.view'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
|
||||||
|
export abstract class AbstractTransactionRecipeRole {
|
||||||
|
protected transactionBody: TransactionBody
|
||||||
|
public constructor(protected self: Transaction) {
|
||||||
|
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract transmitToIota(): Promise<Transaction>
|
||||||
|
|
||||||
|
protected getGradidoTransaction(): GradidoTransaction {
|
||||||
|
const transaction = new GradidoTransaction(this.transactionBody)
|
||||||
|
if (!this.self.signature) {
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.MISSING_PARAMETER,
|
||||||
|
'missing signature in transaction recipe',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const signaturePair = new SignaturePair()
|
||||||
|
if (this.self.signature.length !== 64) {
|
||||||
|
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, "signature isn't 64 bytes")
|
||||||
|
}
|
||||||
|
signaturePair.signature = this.self.signature
|
||||||
|
if (this.transactionBody.communityRoot) {
|
||||||
|
const publicKey = this.self.community.rootPubkey
|
||||||
|
if (!publicKey) {
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.MISSING_PARAMETER,
|
||||||
|
'missing community public key for community root transaction',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
signaturePair.pubKey = publicKey
|
||||||
|
} else if (this.self.signingAccount) {
|
||||||
|
const publicKey = this.self.signingAccount.derive2Pubkey
|
||||||
|
if (!publicKey) {
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.MISSING_PARAMETER,
|
||||||
|
'missing signing account public key for transaction',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
signaturePair.pubKey = publicKey
|
||||||
|
} else {
|
||||||
|
throw new TransactionError(
|
||||||
|
TransactionErrorType.NOT_FOUND,
|
||||||
|
"signingAccount not exist and it isn't a community root transaction",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (signaturePair.validate()) {
|
||||||
|
transaction.sigMap.sigPair.push(signaturePair)
|
||||||
|
}
|
||||||
|
if (!KeyPair.verify(transaction.bodyBytes, signaturePair)) {
|
||||||
|
logger.debug('invalid signature', new GradidoTransactionLoggingView(transaction))
|
||||||
|
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, 'signature is invalid')
|
||||||
|
}
|
||||||
|
return transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param gradidoTransaction
|
||||||
|
* @param topic
|
||||||
|
* @return iota message id
|
||||||
|
*/
|
||||||
|
protected async sendViaIota(
|
||||||
|
gradidoTransaction: GradidoTransaction,
|
||||||
|
topic: string,
|
||||||
|
): Promise<Buffer> {
|
||||||
|
// protobuf serializing function
|
||||||
|
const messageBuffer = GradidoTransaction.encode(gradidoTransaction).finish()
|
||||||
|
const resultMessage = await iotaSendMessage(
|
||||||
|
messageBuffer,
|
||||||
|
Uint8Array.from(Buffer.from(topic, 'hex')),
|
||||||
|
)
|
||||||
|
logger.info('transmitted Gradido Transaction to Iota', {
|
||||||
|
id: this.self.id,
|
||||||
|
messageId: resultMessage.messageId,
|
||||||
|
})
|
||||||
|
return Buffer.from(resultMessage.messageId, 'hex')
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
|
import { TransactionLogic } from '@/data/Transaction.logic'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
|
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbound Transaction on recipient community, mark the gradidos as received from another community
|
||||||
|
* need to set gradido id from OUTBOUND transaction!
|
||||||
|
*/
|
||||||
|
export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole {
|
||||||
|
public async transmitToIota(): Promise<Transaction> {
|
||||||
|
logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self))
|
||||||
|
const gradidoTransaction = this.getGradidoTransaction()
|
||||||
|
const pairingTransaction = await new TransactionLogic(this.self).findPairTransaction()
|
||||||
|
if (!pairingTransaction.iotaMessageId || pairingTransaction.iotaMessageId.length !== 32) {
|
||||||
|
throw new LogError(
|
||||||
|
'missing iota message id in pairing transaction, was it already send?',
|
||||||
|
new TransactionLoggingView(pairingTransaction),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId
|
||||||
|
this.self.pairingTransactionId = pairingTransaction.id
|
||||||
|
this.self.pairingTransaction = pairingTransaction
|
||||||
|
pairingTransaction.pairingTransactionId = this.self.id
|
||||||
|
|
||||||
|
if (!this.self.otherCommunity) {
|
||||||
|
throw new LogError('missing other community')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.self.iotaMessageId = await this.sendViaIota(
|
||||||
|
gradidoTransaction,
|
||||||
|
this.self.otherCommunity.iotaTopic,
|
||||||
|
)
|
||||||
|
return this.self
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||||
|
|
||||||
|
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||||
|
|
||||||
|
export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole {
|
||||||
|
public async transmitToIota(): Promise<Transaction> {
|
||||||
|
let transactionCrossGroupTypeName = 'LOCAL'
|
||||||
|
if (this.transactionBody) {
|
||||||
|
transactionCrossGroupTypeName = CrossGroupType[this.transactionBody.type]
|
||||||
|
}
|
||||||
|
logger.debug(
|
||||||
|
`transmit ${transactionCrossGroupTypeName} transaction to iota`,
|
||||||
|
new TransactionLoggingView(this.self),
|
||||||
|
)
|
||||||
|
this.self.iotaMessageId = await this.sendViaIota(
|
||||||
|
this.getGradidoTransaction(),
|
||||||
|
this.self.community.iotaTopic,
|
||||||
|
)
|
||||||
|
return this.self
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outbound Transaction on sender community, mark the gradidos as sended out of community
|
||||||
|
*/
|
||||||
|
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
import { Account } from '@entity/Account'
|
||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
import { TestDB } from '@test/TestDB'
|
||||||
|
|
||||||
|
import { CONFIG } from '@/config'
|
||||||
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { Mnemonic } from '@/data/Mnemonic'
|
||||||
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
|
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||||
|
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||||
|
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
|
||||||
|
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransationRecipe.context'
|
||||||
|
|
||||||
|
import { TransmitToIotaContext } from './TransmitToIota.context'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import { communitySeed } from '@test/seeding/Community.seed'
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
|
||||||
|
|
||||||
|
jest.mock('@typeorm/DataSource', () => ({
|
||||||
|
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/client/IotaClient', () => {
|
||||||
|
return {
|
||||||
|
sendMessage: jest.fn().mockReturnValue({
|
||||||
|
messageId: '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
|
||||||
|
const homeCommunityUuid = v4()
|
||||||
|
const foreignCommunityUuid = v4()
|
||||||
|
|
||||||
|
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
|
||||||
|
const foreignKeyPair = new KeyPair(
|
||||||
|
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
|
||||||
|
)
|
||||||
|
|
||||||
|
let moderator: UserSet
|
||||||
|
let firstUser: UserSet
|
||||||
|
let secondUser: UserSet
|
||||||
|
let foreignUser: UserSet
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
describe('interactions/transmitToIota/TransmitToIotaContext', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await TestDB.instance.setupTestDB()
|
||||||
|
await communitySeed(homeCommunityUuid, false)
|
||||||
|
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
|
||||||
|
|
||||||
|
moderator = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
firstUser = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
secondUser = createUserSet(homeCommunityUuid, keyPair)
|
||||||
|
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
|
||||||
|
|
||||||
|
await Account.save([
|
||||||
|
moderator.account,
|
||||||
|
firstUser.account,
|
||||||
|
secondUser.account,
|
||||||
|
foreignUser.account,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await TestDB.instance.teardownTestDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('LOCAL transaction', async () => {
|
||||||
|
const creationTransactionDraft = new TransactionDraft()
|
||||||
|
creationTransactionDraft.amount = new Decimal('2000')
|
||||||
|
creationTransactionDraft.backendTransactionId = 1
|
||||||
|
creationTransactionDraft.createdAt = new Date().toISOString()
|
||||||
|
creationTransactionDraft.linkedUser = moderator.identifier
|
||||||
|
creationTransactionDraft.user = firstUser.identifier
|
||||||
|
creationTransactionDraft.type = InputTransactionType.CREATION
|
||||||
|
creationTransactionDraft.targetDate = new Date().toISOString()
|
||||||
|
const transactionRecipeContext = new CreateTransactionRecipeContext(creationTransactionDraft)
|
||||||
|
await transactionRecipeContext.run()
|
||||||
|
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||||
|
|
||||||
|
const context = new TransmitToIotaContext(transaction)
|
||||||
|
const debugSpy = jest.spyOn(logger, 'debug')
|
||||||
|
await context.run()
|
||||||
|
expect(
|
||||||
|
transaction.iotaMessageId?.compare(
|
||||||
|
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||||
|
),
|
||||||
|
).toBe(0)
|
||||||
|
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
expect.stringContaining('transmit LOCAL transaction to iota'),
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('OUTBOUND transaction', async () => {
|
||||||
|
const crossGroupSendTransactionDraft = new TransactionDraft()
|
||||||
|
crossGroupSendTransactionDraft.amount = new Decimal('100')
|
||||||
|
crossGroupSendTransactionDraft.backendTransactionId = 4
|
||||||
|
crossGroupSendTransactionDraft.createdAt = now.toISOString()
|
||||||
|
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
|
||||||
|
crossGroupSendTransactionDraft.user = firstUser.identifier
|
||||||
|
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
|
||||||
|
const transactionRecipeContext = new CreateTransactionRecipeContext(
|
||||||
|
crossGroupSendTransactionDraft,
|
||||||
|
)
|
||||||
|
await transactionRecipeContext.run()
|
||||||
|
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||||
|
await transaction.save()
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
expect(body.type).toBe(CrossGroupType.OUTBOUND)
|
||||||
|
const context = new TransmitToIotaContext(transaction)
|
||||||
|
const debugSpy = jest.spyOn(logger, 'debug')
|
||||||
|
await context.run()
|
||||||
|
expect(
|
||||||
|
transaction.iotaMessageId?.compare(
|
||||||
|
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||||
|
),
|
||||||
|
).toBe(0)
|
||||||
|
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||||
|
5,
|
||||||
|
expect.stringContaining('transmit OUTBOUND transaction to iota'),
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('INBOUND transaction', async () => {
|
||||||
|
const crossGroupRecvTransactionDraft = new TransactionDraft()
|
||||||
|
crossGroupRecvTransactionDraft.amount = new Decimal('100')
|
||||||
|
crossGroupRecvTransactionDraft.backendTransactionId = 5
|
||||||
|
crossGroupRecvTransactionDraft.createdAt = now.toISOString()
|
||||||
|
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
|
||||||
|
crossGroupRecvTransactionDraft.user = foreignUser.identifier
|
||||||
|
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
|
||||||
|
const transactionRecipeContext = new CreateTransactionRecipeContext(
|
||||||
|
crossGroupRecvTransactionDraft,
|
||||||
|
)
|
||||||
|
await transactionRecipeContext.run()
|
||||||
|
const transaction = transactionRecipeContext.getTransactionRecipe()
|
||||||
|
await transaction.save()
|
||||||
|
// console.log(new TransactionLoggingView(transaction))
|
||||||
|
const body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
expect(body.type).toBe(CrossGroupType.INBOUND)
|
||||||
|
|
||||||
|
const context = new TransmitToIotaContext(transaction)
|
||||||
|
const debugSpy = jest.spyOn(logger, 'debug')
|
||||||
|
await context.run()
|
||||||
|
expect(
|
||||||
|
transaction.iotaMessageId?.compare(
|
||||||
|
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
|
||||||
|
),
|
||||||
|
).toBe(0)
|
||||||
|
expect(debugSpy).toHaveBeenNthCalledWith(
|
||||||
|
7,
|
||||||
|
expect.stringContaining('transmit INBOUND transaction to iota'),
|
||||||
|
expect.objectContaining({}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
|
|
||||||
|
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||||
|
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||||
|
import { logger } from '@/logging/logger'
|
||||||
|
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
import { getDataSource } from '@/typeorm/DataSource'
|
||||||
|
|
||||||
|
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
|
||||||
|
import { InboundTransactionRecipeRole } from './InboundTransactionRecipe.role'
|
||||||
|
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
|
||||||
|
import { OutboundTransactionRecipeRole } from './OutboundTransactionRecipeRole'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @DCI-Context
|
||||||
|
* Context for sending transaction recipe to iota
|
||||||
|
* send every transaction only once to iota!
|
||||||
|
*/
|
||||||
|
export class TransmitToIotaContext {
|
||||||
|
private transactionRecipeRole: AbstractTransactionRecipeRole
|
||||||
|
|
||||||
|
public constructor(transaction: Transaction) {
|
||||||
|
const transactionBody = TransactionBody.fromBodyBytes(transaction.bodyBytes)
|
||||||
|
switch (transactionBody.type) {
|
||||||
|
case CrossGroupType.LOCAL:
|
||||||
|
this.transactionRecipeRole = new LocalTransactionRecipeRole(transaction)
|
||||||
|
break
|
||||||
|
case CrossGroupType.INBOUND:
|
||||||
|
this.transactionRecipeRole = new InboundTransactionRecipeRole(transaction)
|
||||||
|
break
|
||||||
|
case CrossGroupType.OUTBOUND:
|
||||||
|
this.transactionRecipeRole = new OutboundTransactionRecipeRole(transaction)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new LogError('unknown cross group type', transactionBody.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
const transaction = await this.transactionRecipeRole.transmitToIota()
|
||||||
|
logger.debug('transaction sended via iota', new TransactionLoggingView(transaction))
|
||||||
|
// store changes in db
|
||||||
|
// prevent endless loop
|
||||||
|
const pairingTransaction = transaction.pairingTransaction
|
||||||
|
if (pairingTransaction) {
|
||||||
|
transaction.pairingTransaction = undefined
|
||||||
|
await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||||
|
await transactionalEntityManager.save(transaction)
|
||||||
|
await transactionalEntityManager.save(pairingTransaction)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await transaction.save()
|
||||||
|
}
|
||||||
|
logger.info('sended transaction successfully updated in db')
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ export class AccountLoggingView extends AbstractLoggingView {
|
|||||||
id: this.account.id,
|
id: this.account.id,
|
||||||
user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null,
|
user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null,
|
||||||
derivationIndex: this.account.derivationIndex,
|
derivationIndex: this.account.derivationIndex,
|
||||||
derive2pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
|
derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
|
||||||
type: getEnumValue(AddressType, this.account.type),
|
type: getEnumValue(AddressType, this.account.type),
|
||||||
createdAt: this.dateToString(this.account.createdAt),
|
createdAt: this.dateToString(this.account.createdAt),
|
||||||
confirmedAt: this.dateToString(this.account.confirmedAt),
|
confirmedAt: this.dateToString(this.account.confirmedAt),
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public toJSON(showBackendTransactions = true): any {
|
public toJSON(showBackendTransactions = true, deep = 1): any {
|
||||||
return {
|
return {
|
||||||
id: this.self.id,
|
id: this.self.id,
|
||||||
nr: this.self.nr,
|
nr: this.self.nr,
|
||||||
@ -31,16 +31,23 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
|||||||
community: new CommunityLoggingView(this.self.community).toJSON(),
|
community: new CommunityLoggingView(this.self.community).toJSON(),
|
||||||
otherCommunity: this.self.otherCommunity
|
otherCommunity: this.self.otherCommunity
|
||||||
? new CommunityLoggingView(this.self.otherCommunity)
|
? new CommunityLoggingView(this.self.otherCommunity)
|
||||||
: undefined,
|
: { id: this.self.otherCommunityId },
|
||||||
iotaMessageId: this.self.iotaMessageId
|
iotaMessageId: this.self.iotaMessageId
|
||||||
? this.self.iotaMessageId.toString(this.bufferStringFormat)
|
? this.self.iotaMessageId.toString(this.bufferStringFormat)
|
||||||
: undefined,
|
: undefined,
|
||||||
signingAccount: this.self.signingAccount
|
signingAccount: this.self.signingAccount
|
||||||
? new AccountLoggingView(this.self.signingAccount)
|
? new AccountLoggingView(this.self.signingAccount)
|
||||||
: undefined,
|
: { id: this.self.signingAccountId },
|
||||||
recipientAccount: this.self.recipientAccount
|
recipientAccount: this.self.recipientAccount
|
||||||
? new AccountLoggingView(this.self.recipientAccount)
|
? new AccountLoggingView(this.self.recipientAccount)
|
||||||
: undefined,
|
: { id: this.self.recipientAccountId },
|
||||||
|
pairingTransaction:
|
||||||
|
this.self.pairingTransaction && deep === 1
|
||||||
|
? new TransactionLoggingView(this.self.pairingTransaction).toJSON(
|
||||||
|
showBackendTransactions,
|
||||||
|
deep + 1,
|
||||||
|
)
|
||||||
|
: { id: this.self.pairingTransaction },
|
||||||
amount: this.decimalToString(this.self.amount),
|
amount: this.decimalToString(this.self.amount),
|
||||||
accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation),
|
accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation),
|
||||||
accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation),
|
accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export class TransferAmountLoggingView extends AbstractLoggingView {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public toJSON(): any {
|
public toJSON(): any {
|
||||||
return {
|
return {
|
||||||
publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
|
pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
|
||||||
amount: this.self.amount,
|
amount: this.self.amount,
|
||||||
communityId: this.self.communityId,
|
communityId: this.self.communityId,
|
||||||
}
|
}
|
||||||
|
|||||||
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal file
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { LogError } from '@/server/LogError'
|
||||||
|
|
||||||
|
import { InterruptiveSleep } from '../utils/InterruptiveSleep'
|
||||||
|
|
||||||
|
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||||
|
// and ../federation/client/FederationClientFactory.ts
|
||||||
|
/**
|
||||||
|
* A Singleton class defines the `getInstance` method that lets clients access
|
||||||
|
* the unique singleton instance.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
|
export class InterruptiveSleepManager {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
private static instance: InterruptiveSleepManager
|
||||||
|
private interruptiveSleep: Map<string, InterruptiveSleep> = new Map<string, InterruptiveSleep>()
|
||||||
|
private stepSizeMilliseconds = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Singleton's constructor should always be private to prevent direct
|
||||||
|
* construction calls with the `new` operator.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The static method that controls the access to the singleton instance.
|
||||||
|
*
|
||||||
|
* This implementation let you subclass the Singleton class while keeping
|
||||||
|
* just one instance of each subclass around.
|
||||||
|
*/
|
||||||
|
public static getInstance(): InterruptiveSleepManager {
|
||||||
|
if (!InterruptiveSleepManager.instance) {
|
||||||
|
InterruptiveSleepManager.instance = new InterruptiveSleepManager()
|
||||||
|
}
|
||||||
|
return InterruptiveSleepManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only for new created InterruptiveSleepManager Entries!
|
||||||
|
* @param step size in ms in which new! InterruptiveSleepManager check if they where triggered
|
||||||
|
*/
|
||||||
|
public setStepSize(ms: number) {
|
||||||
|
this.stepSizeMilliseconds = ms
|
||||||
|
}
|
||||||
|
|
||||||
|
public interrupt(key: string): void {
|
||||||
|
const interruptiveSleep = this.interruptiveSleep.get(key)
|
||||||
|
if (interruptiveSleep) {
|
||||||
|
interruptiveSleep.interrupt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sleep(key: string, ms: number): Promise<void> {
|
||||||
|
if (!this.interruptiveSleep.has(key)) {
|
||||||
|
this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds))
|
||||||
|
}
|
||||||
|
const interruptiveSleep = this.interruptiveSleep.get(key)
|
||||||
|
if (!interruptiveSleep) {
|
||||||
|
throw new LogError('map entry not exist after setting it')
|
||||||
|
}
|
||||||
|
return interruptiveSleep.sleep(ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
dlt-connector/src/tasks/transmitToIota.ts
Normal file
49
dlt-connector/src/tasks/transmitToIota.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
|
||||||
|
import { TransactionRepository } from '@/data/Transaction.repository'
|
||||||
|
import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context'
|
||||||
|
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
|
||||||
|
|
||||||
|
import { logger } from '../logging/logger'
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let running = true
|
||||||
|
|
||||||
|
export const stopTransmitToIota = (): void => {
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* check for pending transactions:
|
||||||
|
* - if one found call TransmitToIotaContext
|
||||||
|
* - if not, wait 1000 ms and try again
|
||||||
|
* if a new transaction was added, the sleep will be interrupted
|
||||||
|
*/
|
||||||
|
export const transmitToIota = async (): Promise<void> => {
|
||||||
|
logger.info('start iota message transmitter')
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const recipe = await TransactionRepository.getNextPendingTransaction()
|
||||||
|
if (!recipe) break
|
||||||
|
const transmitToIotaContext = new TransmitToIotaContext(recipe)
|
||||||
|
await transmitToIotaContext.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
await InterruptiveSleepManager.getInstance().sleep(
|
||||||
|
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('error while transmitting to iota, retry in 10 seconds ', error)
|
||||||
|
await sleep(10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.error(
|
||||||
|
'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!',
|
||||||
|
)
|
||||||
|
}
|
||||||
31
dlt-connector/src/utils/InterruptiveSleep.ts
Normal file
31
dlt-connector/src/utils/InterruptiveSleep.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Sleep, that can be interrupted
|
||||||
|
* call sleep only for msSteps and than check if interrupt was called
|
||||||
|
*/
|
||||||
|
export class InterruptiveSleep {
|
||||||
|
private interruptSleep = false
|
||||||
|
private msSteps = 10
|
||||||
|
|
||||||
|
constructor(msSteps: number) {
|
||||||
|
this.msSteps = msSteps
|
||||||
|
}
|
||||||
|
|
||||||
|
public interrupt(): void {
|
||||||
|
this.interruptSleep = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sleep(ms: number): Promise<void> {
|
||||||
|
let waited = 0
|
||||||
|
this.interruptSleep = false
|
||||||
|
while (waited < ms && !this.interruptSleep) {
|
||||||
|
await InterruptiveSleep._sleep(this.msSteps)
|
||||||
|
waited += this.msSteps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,45 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { Timestamp } from '@/data/proto/3_3/Timestamp'
|
import { Timestamp } from '@/data/proto/3_3/Timestamp'
|
||||||
|
|
||||||
import { timestampToDate } from './typeConverter'
|
import {
|
||||||
|
base64ToBuffer,
|
||||||
|
iotaTopicFromCommunityUUID,
|
||||||
|
timestampSecondsToDate,
|
||||||
|
timestampToDate,
|
||||||
|
uuid4ToBuffer,
|
||||||
|
} from './typeConverter'
|
||||||
|
|
||||||
describe('utils/typeConverter', () => {
|
describe('utils/typeConverter', () => {
|
||||||
|
it('uuid4ToBuffer', () => {
|
||||||
|
expect(uuid4ToBuffer('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toStrictEqual(
|
||||||
|
Buffer.from('4f28e0815c394ddeb6a43bde71de8d65', 'hex'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('iotaTopicFromCommunityUUID', () => {
|
||||||
|
expect(iotaTopicFromCommunityUUID('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toBe(
|
||||||
|
'3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('timestampToDate', () => {
|
it('timestampToDate', () => {
|
||||||
const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000')
|
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
|
||||||
const timestamp = new Timestamp(now)
|
const timestamp = new Timestamp(now)
|
||||||
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
|
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
|
||||||
expect(timestampToDate(timestamp)).toEqual(now)
|
expect(timestampToDate(timestamp)).toEqual(now)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('timestampSecondsToDate', () => {
|
||||||
|
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
|
||||||
|
const timestamp = new Timestamp(now)
|
||||||
|
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
|
||||||
|
expect(timestampSecondsToDate(timestamp)).toEqual(new Date('Thu, 05 Oct 2023 11:55:18 +0000'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('base64ToBuffer', () => {
|
||||||
|
expect(base64ToBuffer('MTizWQMR/fCoI+FzyqlIe30nXCP6sHEGtLE2TLA4r/0=')).toStrictEqual(
|
||||||
|
Buffer.from('3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd', 'hex'),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
28
dlt-connector/test/seeding/Community.seed.ts
Normal file
28
dlt-connector/test/seeding/Community.seed.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Community } from '@entity/Community'
|
||||||
|
|
||||||
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||||
|
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
|
||||||
|
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||||
|
|
||||||
|
export const communitySeed = async (
|
||||||
|
uuid: string,
|
||||||
|
foreign: boolean,
|
||||||
|
keyPair: KeyPair | undefined = undefined,
|
||||||
|
): Promise<Community> => {
|
||||||
|
const homeCommunityDraft = new CommunityDraft()
|
||||||
|
homeCommunityDraft.uuid = uuid
|
||||||
|
homeCommunityDraft.foreign = foreign
|
||||||
|
homeCommunityDraft.createdAt = new Date().toISOString()
|
||||||
|
const iotaTopic = iotaTopicFromCommunityUUID(uuid)
|
||||||
|
const addCommunityContext = new AddCommunityContext(homeCommunityDraft, iotaTopic)
|
||||||
|
await addCommunityContext.run()
|
||||||
|
|
||||||
|
const community = await Community.findOneOrFail({ where: { iotaTopic } })
|
||||||
|
if (foreign && keyPair) {
|
||||||
|
// that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain
|
||||||
|
keyPair.fillInCommunityKeys(community)
|
||||||
|
await community.save()
|
||||||
|
}
|
||||||
|
return community
|
||||||
|
}
|
||||||
55
dlt-connector/test/seeding/UserSet.seed.ts
Normal file
55
dlt-connector/test/seeding/UserSet.seed.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Account } from '@entity/Account'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
|
import { AccountFactory } from '@/data/Account.factory'
|
||||||
|
import { KeyPair } from '@/data/KeyPair'
|
||||||
|
import { UserFactory } from '@/data/User.factory'
|
||||||
|
import { UserLogic } from '@/data/User.logic'
|
||||||
|
import { AccountType } from '@/graphql/enum/AccountType'
|
||||||
|
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||||
|
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||||
|
|
||||||
|
export type UserSet = {
|
||||||
|
identifier: UserIdentifier
|
||||||
|
user: User
|
||||||
|
account: Account
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUserIdentifier = (userUuid: string, communityUuid: string): UserIdentifier => {
|
||||||
|
const user = new UserIdentifier()
|
||||||
|
user.uuid = userUuid
|
||||||
|
user.communityUuid = communityUuid
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUserAndAccount = (
|
||||||
|
userIdentifier: UserIdentifier,
|
||||||
|
communityKeyPair: KeyPair,
|
||||||
|
): Account => {
|
||||||
|
const accountDraft = new UserAccountDraft()
|
||||||
|
accountDraft.user = userIdentifier
|
||||||
|
accountDraft.createdAt = new Date().toISOString()
|
||||||
|
accountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||||
|
const user = UserFactory.create(accountDraft, communityKeyPair)
|
||||||
|
const userLogic = new UserLogic(user)
|
||||||
|
const account = AccountFactory.createAccountFromUserAccountDraft(
|
||||||
|
accountDraft,
|
||||||
|
userLogic.calculateKeyPair(communityKeyPair),
|
||||||
|
)
|
||||||
|
account.user = user
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createUserSet = (communityUuid: string, communityKeyPair: KeyPair): UserSet => {
|
||||||
|
const identifier = createUserIdentifier(v4(), communityUuid)
|
||||||
|
const account = createUserAndAccount(identifier, communityKeyPair)
|
||||||
|
if (!account.user) {
|
||||||
|
throw Error('user missing')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
identifier,
|
||||||
|
account,
|
||||||
|
user: account.user,
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user