From c4c0c6680e005b1115454e8bdfe3b029abff8557 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Fri, 12 Jan 2024 15:55:24 +0100 Subject: [PATCH 01/51] mostly update backend stuff --- .../FederationVisualizeItem.spec.js | 0 .../FederationVisualizeItem.vue | 0 admin/src/graphql/allCommunities.js | 30 ++++++++ admin/src/graphql/getCommunities.js | 17 ----- admin/src/pages/FederationVisualize.vue | 16 ++-- backend/src/config/index.ts | 2 +- backend/src/graphql/model/Community.ts | 31 ++++++++ .../src/graphql/model/FederatedCommunity.ts | 11 ++- .../src/graphql/resolver/CommunityResolver.ts | 11 ++- .../src/graphql/resolver/util/communities.ts | 59 +++++++++++++++ .../Community.ts | 73 +++++++++++++++++++ .../FederatedCommunity.ts | 58 +++++++++++++++ database/entity/Community.ts | 2 +- database/entity/FederatedCommunity.ts | 2 +- ...82-join_community_federated_communities.ts | 3 + dht-node/src/config/index.ts | 2 +- federation/src/config/index.ts | 2 +- 17 files changed, 283 insertions(+), 36 deletions(-) rename admin/src/components/{Fedaration => Federation}/FederationVisualizeItem.spec.js (100%) rename admin/src/components/{Fedaration => Federation}/FederationVisualizeItem.vue (100%) create mode 100644 admin/src/graphql/allCommunities.js delete mode 100644 admin/src/graphql/getCommunities.js create mode 100644 database/entity/0082-join_community_federated_communities/Community.ts create mode 100644 database/entity/0082-join_community_federated_communities/FederatedCommunity.ts create mode 100644 database/migrations/0082-join_community_federated_communities.ts diff --git a/admin/src/components/Fedaration/FederationVisualizeItem.spec.js b/admin/src/components/Federation/FederationVisualizeItem.spec.js similarity index 100% rename from admin/src/components/Fedaration/FederationVisualizeItem.spec.js rename to admin/src/components/Federation/FederationVisualizeItem.spec.js diff --git a/admin/src/components/Fedaration/FederationVisualizeItem.vue b/admin/src/components/Federation/FederationVisualizeItem.vue similarity index 100% rename from admin/src/components/Fedaration/FederationVisualizeItem.vue rename to admin/src/components/Federation/FederationVisualizeItem.vue diff --git a/admin/src/graphql/allCommunities.js b/admin/src/graphql/allCommunities.js new file mode 100644 index 000000000..62010ec90 --- /dev/null +++ b/admin/src/graphql/allCommunities.js @@ -0,0 +1,30 @@ +import gql from 'graphql-tag' + +export const allCommunities = gql` + query { + allCommunities { + id + foreign + url + publicKey + communityUuid + authenticatedAt + name + description + gmsApiKey + creationDate + createdAt + updatedAt + federatedCommunities { + id + apiVersion + endPoint + lastAnnouncedAt + verifiedAt + lastErrorAt + createdAt + updatedAt + } + } + } +` diff --git a/admin/src/graphql/getCommunities.js b/admin/src/graphql/getCommunities.js deleted file mode 100644 index ccf894f6b..000000000 --- a/admin/src/graphql/getCommunities.js +++ /dev/null @@ -1,17 +0,0 @@ -import gql from 'graphql-tag' - -export const getCommunities = gql` - query { - getCommunities { - id - foreign - publicKey - url - lastAnnouncedAt - verifiedAt - lastErrorAt - createdAt - updatedAt - } - } -` diff --git a/admin/src/pages/FederationVisualize.vue b/admin/src/pages/FederationVisualize.vue index 383cf064d..c6edba5aa 100644 --- a/admin/src/pages/FederationVisualize.vue +++ b/admin/src/pages/FederationVisualize.vue @@ -7,7 +7,7 @@ icon="arrow-clockwise" font-scale="2" :animation="animation" - @click="$apollo.queries.GetCommunities.refresh()" + @click="$apollo.queries.allCommunities.refresh()" data-test="federation-communities-refresh-btn" > @@ -30,9 +30,9 @@ diff --git a/admin/src/components/input/EditableLabel.vue b/admin/src/components/input/EditableLabel.vue new file mode 100644 index 000000000..674fcd3e7 --- /dev/null +++ b/admin/src/components/input/EditableLabel.vue @@ -0,0 +1,64 @@ + + + diff --git a/admin/src/graphql/updateHomeCommunity.js b/admin/src/graphql/updateHomeCommunity.js new file mode 100644 index 000000000..bce09f4d0 --- /dev/null +++ b/admin/src/graphql/updateHomeCommunity.js @@ -0,0 +1,7 @@ +import gql from 'graphql-tag' + +export const updateHomeCommunity = gql` + mutation ($uuid: String!, $gmsApiKey: String!) { + updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) + } +` diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 7cc0affac..b22006a8f 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -69,13 +69,20 @@ "deleted_user": "Alle gelöschten Nutzer", "deny": "Ablehnen", "e_mail": "E-Mail", + "edit": "Bearbeiten", "enabled": "aktiviert", "error": "Fehler", "expired": "abgelaufen", "federation": { + "authenticatedAt": "Verifiziert am:", + "communityUuid": "Community UUID:", "createdAt": "Erstellt am", + "gmsApiKey": "GMS API Key:", + "toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!", "gradidoInstances": "Gradido Instanzen", "lastAnnouncedAt": "letzte Bekanntgabe", + "name": "Name", + "publicKey": "PublicKey:", "url": "Url", "verified": "Verifiziert" }, diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 084e2104f..471bfc50c 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -69,13 +69,20 @@ "deleted_user": "All deleted user", "deny": "Reject", "e_mail": "E-mail", + "edit": "Edit", "enabled": "enabled", "error": "Error", "expired": "expired", "federation": { + "authenticatedAt": "verified at:", + "communityUuid": "Community UUID:", "createdAt": "Created At ", + "gmsApiKey": "GMS API Key:", + "toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!", "gradidoInstances": "Gradido Instances", "lastAnnouncedAt": "Last Announced", + "name": "Name", + "publicKey": "PublicKey:", "url": "Url", "verified": "Verified" }, diff --git a/admin/src/pages/FederationVisualize.vue b/admin/src/pages/FederationVisualize.vue index c6edba5aa..e29e0c02e 100644 --- a/admin/src/pages/FederationVisualize.vue +++ b/admin/src/pages/FederationVisualize.vue @@ -16,12 +16,13 @@ {{ $t('federation.verified') }} {{ $t('federation.url') }} + {{ $t('federation.name') }} {{ $t('federation.lastAnnouncedAt') }} {{ $t('federation.createdAt') }} diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index ef7561c3c..1d376b21f 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -2,29 +2,43 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ObjectType, Field, Int } from 'type-graphql' +import { backendLogger as logger } from '@/server/logger' + import { FederatedCommunity } from './FederatedCommunity' @ObjectType() export class Community { constructor(dbCom: DbCommunity) { - this.id = dbCom.id - this.foreign = dbCom.foreign + 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), + ) + } + this.id = dbCom.id ?? 0 + if (dbCom.foreign !== undefined) { + this.foreign = dbCom.foreign + } this.name = dbCom.name this.description = dbCom.description this.gmsApiKey = dbCom.gmsApiKey - this.url = dbCom.url - this.publicKey = dbCom.publicKey.toString('hex') + if (dbCom.url) { + this.url = dbCom.url + } + if (dbCom.publicKey && dbCom.publicKey.length === 32) { + this.publicKey = dbCom.publicKey.toString('hex') + } this.communityUuid = dbCom.communityUuid this.creationDate = dbCom.creationDate this.createdAt = dbCom.createdAt this.updatedAt = dbCom.updatedAt this.uuid = dbCom.communityUuid this.authenticatedAt = dbCom.authenticatedAt - if (dbCom.federatedCommunities) { - this.federatedCommunities = dbCom.federatedCommunities.map( - (federatedCom: DbFederatedCommunity) => new FederatedCommunity(federatedCom), - ) - } } @Field(() => Int) From 4f99e75d73fb6eb819998582c2017e0fbcaa9a80 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Mon, 15 Jan 2024 20:06:35 +0100 Subject: [PATCH 03/51] add list of federated communities --- ...spec.js => CommunityVisualizeItem.spec.js} | 0 .../Federation/CommunityVisualizeItem.vue | 151 ++++++++++++++++++ .../Federation/FederationVisualizeItem.vue | 115 +------------ admin/src/locales/de.json | 5 +- admin/src/locales/en.json | 5 +- admin/src/pages/FederationVisualize.vue | 6 +- 6 files changed, 170 insertions(+), 112 deletions(-) rename admin/src/components/Federation/{FederationVisualizeItem.spec.js => CommunityVisualizeItem.spec.js} (100%) create mode 100644 admin/src/components/Federation/CommunityVisualizeItem.vue diff --git a/admin/src/components/Federation/FederationVisualizeItem.spec.js b/admin/src/components/Federation/CommunityVisualizeItem.spec.js similarity index 100% rename from admin/src/components/Federation/FederationVisualizeItem.spec.js rename to admin/src/components/Federation/CommunityVisualizeItem.spec.js diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue new file mode 100644 index 000000000..0eb55ae59 --- /dev/null +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -0,0 +1,151 @@ + + diff --git a/admin/src/components/Federation/FederationVisualizeItem.vue b/admin/src/components/Federation/FederationVisualizeItem.vue index 97a248aaa..7b2d6c5d8 100644 --- a/admin/src/components/Federation/FederationVisualizeItem.vue +++ b/admin/src/components/Federation/FederationVisualizeItem.vue @@ -1,73 +1,24 @@ diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index b22006a8f..0fb43e48f 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -74,6 +74,7 @@ "error": "Fehler", "expired": "abgelaufen", "federation": { + "apiVersion": "API Version", "authenticatedAt": "Verifiziert am:", "communityUuid": "Community UUID:", "createdAt": "Erstellt am", @@ -81,10 +82,12 @@ "toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!", "gradidoInstances": "Gradido Instanzen", "lastAnnouncedAt": "letzte Bekanntgabe", + "lastErrorAt": "Letzer Fehler am", "name": "Name", "publicKey": "PublicKey:", "url": "Url", - "verified": "Verifiziert" + "verified": "Verifiziert", + "verifiedAt": "Verifiziert am" }, "firstname": "Vorname", "footer": { diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 471bfc50c..b2073a1e2 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -74,6 +74,7 @@ "error": "Error", "expired": "expired", "federation": { + "apiVersion": "API Version", "authenticatedAt": "verified at:", "communityUuid": "Community UUID:", "createdAt": "Created At ", @@ -81,10 +82,12 @@ "toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!", "gradidoInstances": "Gradido Instances", "lastAnnouncedAt": "Last Announced", + "lastErrorAt": "last error at", "name": "Name", "publicKey": "PublicKey:", "url": "Url", - "verified": "Verified" + "verified": "Verified", + "verifiedAt": "Verified at" }, "firstname": "Firstname", "footer": { diff --git a/admin/src/pages/FederationVisualize.vue b/admin/src/pages/FederationVisualize.vue index e29e0c02e..5736b1a0e 100644 --- a/admin/src/pages/FederationVisualize.vue +++ b/admin/src/pages/FederationVisualize.vue @@ -25,7 +25,7 @@ :key="item.publicKey" :variant="!item.foreign ? 'primary' : 'warning'" > - + @@ -33,12 +33,12 @@ diff --git a/admin/src/graphql/updateHomeCommunity.js b/admin/src/graphql/updateHomeCommunity.js index bce09f4d0..a43d6edd2 100644 --- a/admin/src/graphql/updateHomeCommunity.js +++ b/admin/src/graphql/updateHomeCommunity.js @@ -2,6 +2,8 @@ import gql from 'graphql-tag' export const updateHomeCommunity = gql` mutation ($uuid: String!, $gmsApiKey: String!) { - updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) + updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) { + id + } } ` From cc617eb827ebcbf8a04d77a56d754b22ca2cc1cc Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 16 Jan 2024 12:17:50 +0100 Subject: [PATCH 05/51] fix after save behaviour --- admin/src/components/Federation/CommunityVisualizeItem.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue index 0eb55ae59..c99f38ec5 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.vue +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -27,7 +27,7 @@ {{ $t('federation.gmsApiKey') }}  @@ -79,8 +79,12 @@ export default { formatDistanceToNow, locale: this.$i18n.locale, details: false, + gmsApiKey: '', } }, + created() { + this.gmsApiKey = this.item.gmsApiKey + }, computed: { verified() { return ( @@ -131,6 +135,7 @@ export default { this.details = !this.details }, handleSaveGsmApiKey(gmsApiKey) { + this.gmsApiKey = gmsApiKey this.$apollo .mutate({ mutation: updateHomeCommunity, From 7494429aad901e39d54db58f2372c965455da94c Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 16 Jan 2024 16:45:25 +0100 Subject: [PATCH 06/51] fix lint and admin tests --- .vscode/launch.json | 12 ++ admin/jest.config.js | 7 +- admin/package.json | 1 + .../Federation/CommunityVisualizeItem.spec.js | 148 ++++++++++++------ .../Federation/CommunityVisualizeItem.vue | 6 +- .../Federation/FederationVisualizeItem.vue | 6 +- .../components/input/EditableLabel.spec.js | 83 ++++++++++ admin/src/locales/de.json | 1 - admin/src/locales/en.json | 1 - admin/src/pages/FederationVisualize.spec.js | 98 +++++++----- .../src/graphql/resolver/util/communities.ts | 1 - 11 files changed, 270 insertions(+), 94 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 admin/src/components/input/EditableLabel.spec.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..5a60c21e2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node-terminal", + "name": "Admin: test", + "request": "launch", + "command": "yarn run test", + "cwd": "${workspaceFolder}/admin" + } + ] +} \ No newline at end of file diff --git a/admin/jest.config.js b/admin/jest.config.js index 253c905de..a0c8ae412 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -9,7 +9,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 96, + lines: 95, }, }, moduleFileExtensions: [ @@ -31,6 +31,9 @@ module.exports = { setupFiles: ['/test/testSetup.js', 'jest-canvas-mock'], testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], - transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], + transformIgnorePatterns: [ + '/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 } diff --git a/admin/package.json b/admin/package.json index e34136e4b..3c1e666e8 100644 --- a/admin/package.json +++ b/admin/package.json @@ -15,6 +15,7 @@ "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "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" }, "dependencies": { diff --git a/admin/src/components/Federation/CommunityVisualizeItem.spec.js b/admin/src/components/Federation/CommunityVisualizeItem.spec.js index 6058cc6f4..2c5065ba0 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.spec.js +++ b/admin/src/components/Federation/CommunityVisualizeItem.spec.js @@ -1,36 +1,83 @@ import { mount } from '@vue/test-utils' -import FederationVisualizeItem from './FederationVisualizeItem.vue' +import Vuex from 'vuex' +import CommunityVisualizeItem from './CommunityVisualizeItem.vue' const localVue = global.localVue +localVue.use(Vuex) const today = new Date() const createdDate = new Date() createdDate.setDate(createdDate.getDate() - 3) +// Mock für den Vuex-Store +const store = new Vuex.Store({ + state: { + moderator: { + roles: ['ADMIN'], + }, + }, +}) + let propsData = { item: { - id: 7590, + id: 1, foreign: false, - publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', - url: 'http://localhost/api/2_0', - lastAnnouncedAt: createdDate, - verifiedAt: today, - lastErrorAt: null, + url: 'http://localhost/api/', + publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2', + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + name: 'Gradido Test', + description: 'Gradido Community zum testen', + gmsApiKey: '', + creationDate: 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 = { + $t: (key) => key, $i18n: { locale: 'en', }, } -describe('FederationVisualizeItem', () => { +describe('CommunityVisualizeItem', () => { let wrapper const Wrapper = () => { - return mount(FederationVisualizeItem, { localVue, mocks, propsData }) + return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store }) } describe('mount', () => { @@ -39,19 +86,35 @@ describe('FederationVisualizeItem', () => { }) 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', () => { it('has the url', () => { - expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe( - 'http://localhost/api/2_0', + expect(wrapper.find('.row > div:nth-child(2) > div > a').text()).toBe( + 'http://localhost/api/', ) }) it('has the public key', () => { 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('lastAnnouncedAt', () => { 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') }) + 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', () => { beforeEach(() => { propsData = { @@ -163,9 +223,9 @@ describe('FederationVisualizeItem', () => { foreign: false, publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', url: 'http://localhost/api/2_0', - lastAnnouncedAt: createdDate, - verifiedAt: null, - lastErrorAt: null, + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + creationDate: null, createdAt: null, updatedAt: null, }, diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue index c99f38ec5..4a5d9e67e 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.vue +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -12,7 +12,7 @@ {{ lastAnnouncedAt }} {{ createdAt }} - + @@ -87,6 +87,9 @@ export default { }, computed: { verified() { + if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) { + return false + } return ( this.item.federatedCommunities.filter( (federatedCommunity) => @@ -101,6 +104,7 @@ export default { 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) => { diff --git a/admin/src/components/Federation/FederationVisualizeItem.vue b/admin/src/components/Federation/FederationVisualizeItem.vue index cbfe53ef0..9172f9a94 100644 --- a/admin/src/components/Federation/FederationVisualizeItem.vue +++ b/admin/src/components/Federation/FederationVisualizeItem.vue @@ -35,7 +35,11 @@ export default { methods: { distanceDate(dateString) { return dateString - ? formatDistanceToNow(new Date(dateString), { locale: locales[this.$i18n.locale] }) + ? formatDistanceToNow(new Date(dateString), { + includeSecond: true, + addSuffix: true, + locale: locales[this.$i18n.locale], + }) : '' }, }, diff --git a/admin/src/components/input/EditableLabel.spec.js b/admin/src/components/input/EditableLabel.spec.js new file mode 100644 index 000000000..0b2462b30 --- /dev/null +++ b/admin/src/components/input/EditableLabel.spec.js @@ -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') + }) +}) diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 0fb43e48f..9f808f70c 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -69,7 +69,6 @@ "deleted_user": "Alle gelöschten Nutzer", "deny": "Ablehnen", "e_mail": "E-Mail", - "edit": "Bearbeiten", "enabled": "aktiviert", "error": "Fehler", "expired": "abgelaufen", diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index b2073a1e2..b1f020921 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -69,7 +69,6 @@ "deleted_user": "All deleted user", "deny": "Reject", "e_mail": "E-mail", - "edit": "Edit", "enabled": "enabled", "error": "Error", "expired": "expired", diff --git a/admin/src/pages/FederationVisualize.spec.js b/admin/src/pages/FederationVisualize.spec.js index 67d6dffde..a089ef8fe 100644 --- a/admin/src/pages/FederationVisualize.spec.js +++ b/admin/src/pages/FederationVisualize.spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils' import FederationVisualize from './FederationVisualize' import VueApollo from 'vue-apollo' import { createMockClient } from 'mock-apollo-client' -import { getCommunities } from '@/graphql/getCommunities' +import { allCommunities } from '@/graphql/allCommunities' import { toastErrorSpy } from '../../test/testSetup' const mockClient = createMockClient() @@ -25,42 +25,54 @@ const mocks = { const defaultData = () => { return { - getCommunities: [ + allCommunities: [ { - id: 1776, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/2_0', - lastAnnouncedAt: '2023-04-07T12:27:24.037Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.254Z', - updatedAt: null, - __typename: 'Community', - }, - { - id: 1775, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/1_1', - lastAnnouncedAt: '2023-04-07T12:27:24.023Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.234Z', - updatedAt: null, - __typename: 'Community', - }, - { - id: 1774, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/1_0', - lastAnnouncedAt: '2023-04-07T12:27:24.009Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.218Z', - updatedAt: null, - __typename: 'Community', + id: 1, + foreign: false, + url: 'http://localhost/api/', + publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2', + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + name: 'Gradido Test', + description: 'Gradido Community zum testen', + gmsApiKey: '', + creationDate: '2024-01-09T15:56:40.592Z', + createdAt: '2024-01-09T15:56:40.595Z', + updatedAt: '2024-01-16T11:17:15.000Z', + federatedCommunities: [ + { + id: 2046, + apiVersion: '2_0', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.544Z', + 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, + __typename: 'FederatedCommunity', + }, + { + 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', () => { let wrapper - const getCommunitiesMock = jest.fn() + const allCommunitiesMock = jest.fn() mockClient.setRequestHandler( - getCommunities, - getCommunitiesMock + allCommunities, + allCommunitiesMock .mockRejectedValueOnce({ message: 'Ouch!' }) .mockResolvedValue({ data: defaultData() }), ) @@ -95,7 +107,7 @@ describe('FederationVisualize', () => { describe('sever success', () => { it('sends query to Apollo when created', () => { - expect(getCommunitiesMock).toBeCalled() + expect(allCommunitiesMock).toBeCalled() }) 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) }) - it('renders 3 community list items', () => { - expect(wrapper.findAll('.list-group-item').length).toBe(3) + it('renders 1 community list item', () => { + expect(wrapper.findAll('.list-group-item').length).toBe(1) }) describe('cklicking the refresh button', () => { @@ -117,7 +129,7 @@ describe('FederationVisualize', () => { }) it('calls the API', async () => { - expect(getCommunitiesMock).toBeCalled() + expect(allCommunitiesMock).toBeCalled() }) }) }) diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index b8f23b50e..e85357991 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -3,7 +3,6 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCom import { Paginated } from '@arg/Paginated' -import { Order } from '@/graphql/enum/Order' import { LogError } from '@/server/LogError' import { Connection } from '@/typeorm/connection' From d5b0a2060b33c4ef0c4b95bb04b3fa160eb443e5 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 16 Jan 2024 18:28:55 +0100 Subject: [PATCH 07/51] fix test --- .../resolver/CommunityResolver.test.ts | 134 +++++++++++++----- backend/src/seeds/graphql/queries.ts | 2 +- 2 files changed, 101 insertions(+), 35 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 3cdc04568..c3dbd4cda 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -79,7 +79,10 @@ describe('CommunityResolver', () => { homeCom1 = DbFederatedCommunity.create() homeCom1.foreign = false - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom1.publicKey = Buffer.from( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + 'hex', + ) homeCom1.apiVersion = '1_0' homeCom1.endPoint = 'http://localhost/api' homeCom1.createdAt = new Date() @@ -87,7 +90,10 @@ describe('CommunityResolver', () => { homeCom2 = DbFederatedCommunity.create() homeCom2.foreign = false - homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom2.publicKey = Buffer.from( + '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', + 'hex', + ) homeCom2.apiVersion = '1_1' homeCom2.endPoint = 'http://localhost/api' homeCom2.createdAt = new Date() @@ -95,7 +101,10 @@ describe('CommunityResolver', () => { homeCom3 = DbFederatedCommunity.create() homeCom3.foreign = false - homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom3.publicKey = Buffer.from( + 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', + 'hex', + ) homeCom3.apiVersion = '2_0' homeCom3.endPoint = 'http://localhost/api' homeCom3.createdAt = new Date() @@ -109,8 +118,10 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/2_0'), + publicKey: expect.stringMatching( + 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', + ), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -120,8 +131,10 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/1_1'), + publicKey: expect.stringMatching( + '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', + ), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -131,8 +144,10 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/1_0'), + publicKey: expect.stringMatching( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + ), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -151,7 +166,10 @@ describe('CommunityResolver', () => { foreignCom1 = DbFederatedCommunity.create() foreignCom1.foreign = true - foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom1.publicKey = Buffer.from( + '2e3c58f8fb7d9e4c1f2e3a9ec51cf4b1473d9b6b422853ac4c28e68ec41db4f3', + 'hex', + ) foreignCom1.apiVersion = '1_0' foreignCom1.endPoint = 'http://remotehost/api' foreignCom1.createdAt = new Date() @@ -159,7 +177,10 @@ describe('CommunityResolver', () => { foreignCom2 = DbFederatedCommunity.create() foreignCom2.foreign = true - foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom2.publicKey = Buffer.from( + '9c74d200b9c8b7210db8e65a58b869e4a8c84eb5b2b8b8e8d1ed74e6d9f5b7a1', + 'hex', + ) foreignCom2.apiVersion = '1_1' foreignCom2.endPoint = 'http://remotehost/api' foreignCom2.createdAt = new Date() @@ -167,7 +188,10 @@ describe('CommunityResolver', () => { foreignCom3 = DbFederatedCommunity.create() foreignCom3.foreign = true - foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom3.publicKey = Buffer.from( + '13fd4edf5b708b5815f8d7d0c0ba34e6aa810f3f187f0bd3d25e0e74315ec16a', + 'hex', + ) foreignCom3.apiVersion = '1_2' foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.createdAt = new Date() @@ -181,8 +205,10 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/2_0'), + publicKey: expect.stringMatching( + 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', + ), + endPoint: expect.stringMatching('http://localhost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -192,8 +218,10 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/1_1'), + publicKey: expect.stringMatching( + '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', + ), + endPoint: expect.stringMatching('http://localhost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -203,8 +231,10 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), - url: expect.stringMatching('http://localhost/api/1_0'), + publicKey: expect.stringMatching( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + ), + endPoint: expect.stringMatching('http://localhost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -214,8 +244,10 @@ describe('CommunityResolver', () => { { id: 6, foreign: foreignCom3.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), - url: expect.stringMatching('http://remotehost/api/1_2'), + publicKey: expect.stringMatching( + '13fd4edf5b708b5815f8d7d0c0ba34e6aa810f3f187f0bd3d25e0e74315ec16a', + ), + endPoint: expect.stringMatching('http://remotehost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -225,8 +257,10 @@ describe('CommunityResolver', () => { { id: 5, foreign: foreignCom2.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), - url: expect.stringMatching('http://remotehost/api/1_1'), + publicKey: expect.stringMatching( + '9c74d200b9c8b7210db8e65a58b869e4a8c84eb5b2b8b8e8d1ed74e6d9f5b7a1', + ), + endPoint: expect.stringMatching('http://remotehost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -236,8 +270,10 @@ describe('CommunityResolver', () => { { id: 4, foreign: foreignCom1.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), - url: expect.stringMatching('http://remotehost/api/1_0'), + publicKey: expect.stringMatching( + '2e3c58f8fb7d9e4c1f2e3a9ec51cf4b1473d9b6b422853ac4c28e68ec41db4f3', + ), + endPoint: expect.stringMatching('http://remotehost/api'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -281,7 +317,10 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom1.publicKey = Buffer.from( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + 'hex', + ) homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity') homeCom1.communityUuid = 'HomeCom-UUID' homeCom1.authenticatedAt = new Date() @@ -319,7 +358,10 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom1.publicKey = Buffer.from( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + 'hex', + ) homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity') homeCom1.communityUuid = 'HomeCom-UUID' homeCom1.authenticatedAt = new Date() @@ -331,8 +373,14 @@ describe('CommunityResolver', () => { foreignCom1 = DbCommunity.create() foreignCom1.foreign = true foreignCom1.url = 'http://stage-2.gradido.net/api' - foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community') - foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community') + foreignCom1.publicKey = Buffer.from( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + 'hex', + ) + foreignCom1.privateKey = Buffer.from( + 'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a', + 'hex', + ) // foreignCom1.communityUuid = 'Stage2-Com-UUID' // foreignCom1.authenticatedAt = new Date() foreignCom1.name = 'Stage-2_Community-name' @@ -343,8 +391,14 @@ describe('CommunityResolver', () => { foreignCom2 = DbCommunity.create() foreignCom2.foreign = true foreignCom2.url = 'http://stage-3.gradido.net/api' - foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community') - foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community') + foreignCom2.publicKey = Buffer.from( + 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', + 'hex', + ) + foreignCom2.privateKey = Buffer.from( + 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', + 'hex', + ) foreignCom2.communityUuid = 'Stage3-Com-UUID' foreignCom2.authenticatedAt = new Date() foreignCom2.name = 'Stage-3_Community-name' @@ -410,8 +464,14 @@ describe('CommunityResolver', () => { foreignCom1 = DbCommunity.create() foreignCom1.foreign = true foreignCom1.url = 'http://stage-2.gradido.net/api' - foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community') - foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community') + foreignCom1.publicKey = Buffer.from( + '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', + 'hex', + ) + foreignCom1.privateKey = Buffer.from( + 'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a', + 'hex', + ) // foreignCom1.communityUuid = 'Stage2-Com-UUID' // foreignCom1.authenticatedAt = new Date() foreignCom1.name = 'Stage-2_Community-name' @@ -422,8 +482,14 @@ describe('CommunityResolver', () => { foreignCom2 = DbCommunity.create() foreignCom2.foreign = true foreignCom2.url = 'http://stage-3.gradido.net/api' - foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community') - foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community') + foreignCom2.publicKey = Buffer.from( + 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', + 'hex', + ) + foreignCom2.privateKey = Buffer.from( + 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', + 'hex', + ) foreignCom2.communityUuid = 'Stage3-Com-UUID' foreignCom2.authenticatedAt = new Date() foreignCom2.name = 'Stage-3_Community-name' diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 44c3c35e1..05ef0c740 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -156,7 +156,7 @@ export const getCommunities = gql` id foreign publicKey - url + endPoint lastAnnouncedAt verifiedAt lastErrorAt From 4c389635f14c09f0e77e971094de0f722865e4f3 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 12:15:26 +0100 Subject: [PATCH 08/51] import code from dlt main branch, rename --- dlt-connector/src/client/IotaClient.ts | 21 +++++-- dlt-connector/src/data/const.ts | 1 + .../graphql/resolver/TransactionsResolver.ts | 6 +- dlt-connector/src/index.ts | 8 +++ .../community/HomeCommunity.role.ts | 6 +- .../transmitToIota/TransmitToIota.context.ts | 41 ++++++++++++ .../src/manager/InterruptiveSleepManager.ts | 63 +++++++++++++++++++ dlt-connector/src/tasks/transmitToIota.ts | 50 +++++++++++++++ dlt-connector/src/utils/InterruptiveSleep.ts | 31 +++++++++ 9 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 dlt-connector/src/data/const.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts create mode 100644 dlt-connector/src/manager/InterruptiveSleepManager.ts create mode 100644 dlt-connector/src/tasks/transmitToIota.ts create mode 100644 dlt-connector/src/utils/InterruptiveSleep.ts diff --git a/dlt-connector/src/client/IotaClient.ts b/dlt-connector/src/client/IotaClient.ts index f6e6b1772..0e5f24739 100644 --- a/dlt-connector/src/client/IotaClient.ts +++ b/dlt-connector/src/client/IotaClient.ts @@ -2,17 +2,19 @@ import { ClientBuilder } from '@iota/client' import { MessageWrapper } from '@iota/client/lib/types' import { CONFIG } from '@/config' - const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build() /** * 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} topic - the iota topic to which the message will be sended * @return {Promise} the iota message typed */ -function sendMessage(message: string | Uint8Array): Promise { - return client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit() +function sendMessage( + message: string | Uint8Array, + topic: string | Uint8Array, +): Promise { + return client.message().index(topic).data(message).submit() } /** @@ -24,7 +26,16 @@ function receiveMessage(messageId: string): Promise { return client.getMessage().data(messageId) } -export { sendMessage, receiveMessage } +function receiveAllMessagesForTopic(topic: string | Uint8Array): Promise { + return client.getMessage().index(topic) +} + +async function getIotaMilestone(messageId: string): Promise { + const metadata = await client.getMessage().metadata(messageId) + return metadata.referencedByMilestoneIndex +} + +export { sendMessage, receiveMessage, receiveAllMessagesForTopic, getIotaMilestone } /** * example for message: diff --git a/dlt-connector/src/data/const.ts b/dlt-connector/src/data/const.ts new file mode 100644 index 000000000..82470e8d4 --- /dev/null +++ b/dlt-connector/src/data/const.ts @@ -0,0 +1 @@ +export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota' diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 6a5017fb1..5cd122644 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,12 +1,13 @@ +import { TransactionDraft } from '@input/TransactionDraft' import { Resolver, Arg, Mutation } from 'type-graphql' -import { TransactionDraft } from '@input/TransactionDraft' - +import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { TransactionRepository } from '@/data/Transaction.repository' import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context' import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view' import { logger } from '@/logging/logger' import { TransactionLoggingView } from '@/logging/TransactionLogging.view' +import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { LogError } from '@/server/LogError' import { TransactionError } from '../model/TransactionError' @@ -48,6 +49,7 @@ export class TransactionResolver { // we can store the transaction and with that automatic the backend transaction await transactionRecipe.save() } + InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) return new TransactionResult(new TransactionRecipe(transactionRecipe)) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index c72978b35..b998430fd 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -2,16 +2,24 @@ import { CONFIG } from '@/config' import createServer from './server/createServer' +import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' async function main() { // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() + // loop run all the time, check for new transaction for sending to iota + void transmitToIota() app.listen(CONFIG.DLT_CONNECTOR_PORT, () => { // eslint-disable-next-line no-console console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`) }) + + process.on('exit', () => { + // Add shutdown logic here. + stopTransmitToIota() + }) } main().catch((e) => { diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 7a4798368..647c5d397 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -3,6 +3,7 @@ import { Transaction } from '@entity/Transaction' import { CONFIG } from '@/config' import { AccountFactory } from '@/data/Account.factory' +import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { KeyPair } from '@/data/KeyPair' import { Mnemonic } from '@/data/Mnemonic' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' @@ -10,6 +11,7 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionError } from '@/graphql/model/TransactionError' import { CommunityLoggingView } from '@/logging/CommunityLogging.view' import { logger } from '@/logging/logger' +import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' @@ -36,12 +38,14 @@ export class HomeCommunityRole extends CommunityRole { public async store(): Promise { try { - return await getDataSource().transaction(async (transactionalEntityManager) => { + const community = await getDataSource().transaction(async (transactionalEntityManager) => { const community = await transactionalEntityManager.save(this.self) await transactionalEntityManager.save(this.transactionRecipe) logger.debug('store home community', new CommunityLoggingView(community)) return community }) + InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY) + return community } catch (error) { logger.error('error saving home community into db: %s', error) throw new TransactionError( diff --git a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts new file mode 100644 index 000000000..fb38428a2 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts @@ -0,0 +1,41 @@ +import { Transaction } from '@entity/Transaction' + +import { logger } from '@/logging/logger' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' + +/** + * @DCI-Context + * Context for sending transaction recipe to iota + * send every transaction only once to iota! + */ +export class TransmitToIotaContext { + // eslint-disable-next-line no-useless-constructor + public constructor(private transaction: Transaction) { + + } + + public async run(): Promise { + logger.info('transmit to iota', new TransactionLoggingView(this.transaction)) + const recipeController = new TransactionRecipe(recipe) + const { transaction, body } = recipeController.getGradidoTransaction() + const messageBuffer = GradidoTransaction.encode(transaction).finish() + + if (body.type === CrossGroupType.LOCAL) { + const resultMessage = await iotaSendMessage( + messageBuffer, + Buffer.from(recipe.community.iotaTopic, 'hex'), + ) + recipe.iotaMessageId = Buffer.from(resultMessage.messageId, 'hex') + logger.info('transmitted Gradido Transaction to Iota', { + id: recipe.id, + messageId: resultMessage.messageId, + }) + await getDataSource().manager.save(recipe) + } else { + throw new TransactionError( + TransactionErrorType.NOT_IMPLEMENTED_YET, + 'other as crossGroupType Local not implemented yet', + ) + } + } +} diff --git a/dlt-connector/src/manager/InterruptiveSleepManager.ts b/dlt-connector/src/manager/InterruptiveSleepManager.ts new file mode 100644 index 000000000..3fc5a9b88 --- /dev/null +++ b/dlt-connector/src/manager/InterruptiveSleepManager.ts @@ -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 = new Map() + 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 { + if (!this.interruptiveSleep.has(key)) { + this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds)) + } + const cond = this.interruptiveSleep.get(key) + if (!cond) { + throw new LogError('map entry not exist after setting it') + } + return cond.sleep(ms) + } +} diff --git a/dlt-connector/src/tasks/transmitToIota.ts b/dlt-connector/src/tasks/transmitToIota.ts new file mode 100644 index 000000000..5a0545bc8 --- /dev/null +++ b/dlt-connector/src/tasks/transmitToIota.ts @@ -0,0 +1,50 @@ +import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' +import { TransactionRepository } from '@/data/Transaction.repository' +import { TransmitToIotaContext } from '@/interactions/backendToDb/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 => { + 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, + 1000, + ) + } catch (error) { + logger.error('error while transmitting to iota, retry in 10 seconds ', error) + await sleep(10000) + } + } + logger.info( + 'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!', + ) +} diff --git a/dlt-connector/src/utils/InterruptiveSleep.ts b/dlt-connector/src/utils/InterruptiveSleep.ts new file mode 100644 index 000000000..c21e57db9 --- /dev/null +++ b/dlt-connector/src/utils/InterruptiveSleep.ts @@ -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 { + let waited = 0 + this.interruptSleep = false + while (waited < ms && !this.interruptSleep) { + await InterruptiveSleep._sleep(this.msSteps) + waited += this.msSteps + } + } +} From 35be61a9c865e32cb9356f73058aeac9288574e1 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 18:40:34 +0100 Subject: [PATCH 09/51] create context and roles for transmit to iota --- dlt-connector/src/client/IotaClient.test.ts | 2 +- dlt-connector/src/data/KeyPair.ts | 5 +- dlt-connector/src/data/Transaction.logic.ts | 161 ++++++++++++++++++ .../src/data/proto/3_3/GradidoTransaction.ts | 13 +- .../src/data/proto/3_3/TransactionBody.ts | 15 ++ .../graphql/resolver/TransactionsResolver.ts | 3 +- .../transmitToIota/TransmitToIota.context.ts | 41 ----- .../AbstractTransactionRecipe.role.ts | 90 ++++++++++ .../InboundTransactionRecipe.role.ts | 40 +++++ .../LocalTransactionRecipe.role.ts | 25 +++ .../OutboundTransactionRecipeRole.ts | 6 + .../transmitToIota/TransmitToIota.context.ts | 42 +++++ dlt-connector/src/tasks/transmitToIota.ts | 5 +- .../Transaction.ts | 2 +- 14 files changed, 389 insertions(+), 61 deletions(-) create mode 100644 dlt-connector/src/data/Transaction.logic.ts delete mode 100644 dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts diff --git a/dlt-connector/src/client/IotaClient.test.ts b/dlt-connector/src/client/IotaClient.test.ts index 2ceaa085e..5bee71b2e 100644 --- a/dlt-connector/src/client/IotaClient.test.ts +++ b/dlt-connector/src/client/IotaClient.test.ts @@ -50,7 +50,7 @@ jest.mock('@iota/client', () => { describe('Iota Tests', () => { it('test mocked sendDataMessage', async () => { - const result = await sendMessage('Test Message') + const result = await sendMessage('Test Message', 'topic') expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710') }) diff --git a/dlt-connector/src/data/KeyPair.ts b/dlt-connector/src/data/KeyPair.ts index 59e9a5066..dc2dd1bab 100644 --- a/dlt-connector/src/data/KeyPair.ts +++ b/dlt-connector/src/data/KeyPair.ts @@ -6,6 +6,7 @@ import { LogError } from '@/server/LogError' import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519' import { Mnemonic } from './Mnemonic' +import { SignaturePair } from './proto/3_3/SignaturePair' /** * 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()) } - public verify(message: Buffer, signature: Buffer): boolean { - return verify(message, signature, this.getExtendPublicKey()) + public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean { + return verify(message, signature, pubKey) } } diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts new file mode 100644 index 000000000..62f7c3732 --- /dev/null +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -0,0 +1,161 @@ +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 { + const type = this.getBody().type + if (type === CrossGroupType.LOCAL) { + throw new LogError("local transaction don't has a pairing transaction") + } + + // check if already on entity + if (this.self.paringTransaction) { + return this.self.paringTransaction + } + + if (this.self.paringTransactionId) { + const pairingTransaction = await Transaction.findOneBy({ id: this.self.paringTransactionId }) + 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 paring 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.communityId || + this.self.otherCommunityId !== otherTransaction.otherCommunityId || + this.self.amount !== otherTransaction.amount || + this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || + this.self.createdAt !== otherTransaction.createdAt + ) { + logger.debug('transaction a and b are not pairs', { + a: new TransactionLoggingView(this.self), + b: new TransactionLoggingView(otherTransaction), + }) + return false + } + const body = this.getBody() + const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes) + /** + * both can be Cross + * one can be OUTBOUND and one can be INBOUND + * no one can be LOCAL + */ + if ( + (body.type === otherBody.type && body.type !== CrossGroupType.CROSS) || + body.type === CrossGroupType.LOCAL || + otherBody.type === CrossGroupType.LOCAL + ) { + logger.info("cross group types don't match", { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + 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), + b: new TransactionBodyLoggingView(otherBody), + }) + } + if (type !== otherType) { + logger.info("transaction types don't match", { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + if ( + [ + TransactionType.COMMUNITY_ROOT, + TransactionType.GRADIDO_CREATION, + TransactionType.GRADIDO_DEFERRED_TRANSFER, + ].includes(type) + ) { + logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`) + return false + } + if (body.otherGroup === otherBody.otherGroup) { + logger.info('otherGroups are the same', { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + if (body.memo !== otherBody.memo) { + logger.info('memo differ', { + a: new TransactionBodyLoggingView(body), + b: new TransactionBodyLoggingView(otherBody), + }) + return false + } + return true + } + + public getBody(): TransactionBody { + if (!this.transactionBody) { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + } + return this.transactionBody + } +} diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts index f38bcbd1f..f4274407b 100644 --- a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts @@ -1,8 +1,5 @@ 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 { SignatureMap } from './SignatureMap' @@ -46,14 +43,6 @@ export class GradidoTransaction extends Message { } getTransactionBody(): TransactionBody { - try { - 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', - ) - } + return TransactionBody.fromBodyBytes(this.bodyBytes) } } diff --git a/dlt-connector/src/data/proto/3_3/TransactionBody.ts b/dlt-connector/src/data/proto/3_3/TransactionBody.ts index 0c2733606..39d5602ec 100644 --- a/dlt-connector/src/data/proto/3_3/TransactionBody.ts +++ b/dlt-connector/src/data/proto/3_3/TransactionBody.ts @@ -1,8 +1,11 @@ import { Transaction } from '@entity/Transaction' import { Field, Message, OneOf } from 'protobufjs' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionError } from '@/graphql/model/TransactionError' +import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' import { timestampToDate } from '@/utils/typeConverter' @@ -36,6 +39,18 @@ export class TransactionBody extends Message { } } + 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') public memo: string diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 5cd122644..7cc619400 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,6 +1,7 @@ -import { TransactionDraft } from '@input/TransactionDraft' import { Resolver, Arg, Mutation } from 'type-graphql' +import { TransactionDraft } from '@input/TransactionDraft' + import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { TransactionRepository } from '@/data/Transaction.repository' import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context' diff --git a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts deleted file mode 100644 index fb38428a2..000000000 --- a/dlt-connector/src/interactions/backendToDb/transmitToIota/TransmitToIota.context.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Transaction } from '@entity/Transaction' - -import { logger } from '@/logging/logger' -import { TransactionLoggingView } from '@/logging/TransactionLogging.view' - -/** - * @DCI-Context - * Context for sending transaction recipe to iota - * send every transaction only once to iota! - */ -export class TransmitToIotaContext { - // eslint-disable-next-line no-useless-constructor - public constructor(private transaction: Transaction) { - - } - - public async run(): Promise { - logger.info('transmit to iota', new TransactionLoggingView(this.transaction)) - const recipeController = new TransactionRecipe(recipe) - const { transaction, body } = recipeController.getGradidoTransaction() - const messageBuffer = GradidoTransaction.encode(transaction).finish() - - if (body.type === CrossGroupType.LOCAL) { - const resultMessage = await iotaSendMessage( - messageBuffer, - Buffer.from(recipe.community.iotaTopic, 'hex'), - ) - recipe.iotaMessageId = Buffer.from(resultMessage.messageId, 'hex') - logger.info('transmitted Gradido Transaction to Iota', { - id: recipe.id, - messageId: resultMessage.messageId, - }) - await getDataSource().manager.save(recipe) - } else { - throw new TransactionError( - TransactionErrorType.NOT_IMPLEMENTED_YET, - 'other as crossGroupType Local not implemented yet', - ) - } - } -} diff --git a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts new file mode 100644 index 000000000..21285705e --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts @@ -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 | undefined + // eslint-disable-next-line no-useless-constructor + public constructor(protected self: Transaction) {} + + public abstract transmitToIota(): Promise + + protected getGradidoTransaction(): GradidoTransaction { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + 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 { + // 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') + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts new file mode 100644 index 000000000..52e95c12c --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts @@ -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 { + 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.paringTransactionId = pairingTransaction.id + this.self.paringTransaction = pairingTransaction + pairingTransaction.paringTransactionId = 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 + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts new file mode 100644 index 000000000..60cc58ff7 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/LocalTransactionRecipe.role.ts @@ -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 { + 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 + } +} diff --git a/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts b/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts new file mode 100644 index 000000000..a54cd8ec3 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/OutboundTransactionRecipeRole.ts @@ -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 {} diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts new file mode 100644 index 000000000..8e0b32c6c --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -0,0 +1,42 @@ +import { Transaction } from '@entity/Transaction' + +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { LogError } from '@/server/LogError' + +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 { + const transaction = await this.transactionRecipeRole.transmitToIota() + // store changes in db + await transaction.save() + } +} diff --git a/dlt-connector/src/tasks/transmitToIota.ts b/dlt-connector/src/tasks/transmitToIota.ts index 5a0545bc8..89236586e 100644 --- a/dlt-connector/src/tasks/transmitToIota.ts +++ b/dlt-connector/src/tasks/transmitToIota.ts @@ -1,6 +1,6 @@ import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const' import { TransactionRepository } from '@/data/Transaction.repository' -import { TransmitToIotaContext } from '@/interactions/backendToDb/transmitToIota/TransmitToIota.context' +import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context' import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' import { logger } from '../logging/logger' @@ -36,7 +36,6 @@ export const transmitToIota = async (): Promise => { await InterruptiveSleepManager.getInstance().sleep( TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY, - // 1000, 1000, ) } catch (error) { @@ -44,7 +43,7 @@ export const transmitToIota = async (): Promise => { await sleep(10000) } } - logger.info( + logger.error( 'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!', ) } diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts index 922bf81cd..4947c2a2d 100644 --- a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts @@ -23,7 +23,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true }) iotaMessageId?: Buffer - @OneToOne(() => Transaction) + @OneToOne(() => Transaction, { cascade: ['update'] }) // eslint-disable-next-line no-use-before-define paringTransaction?: Transaction From 7de4a3d2840d707487f3d8989e2e36c2bc6c0dd8 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Tue, 23 Jan 2024 18:43:25 +0100 Subject: [PATCH 10/51] remove not used code --- dlt-connector/src/client/IotaClient.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/dlt-connector/src/client/IotaClient.ts b/dlt-connector/src/client/IotaClient.ts index 0e5f24739..3f2d318fa 100644 --- a/dlt-connector/src/client/IotaClient.ts +++ b/dlt-connector/src/client/IotaClient.ts @@ -26,16 +26,7 @@ function receiveMessage(messageId: string): Promise { return client.getMessage().data(messageId) } -function receiveAllMessagesForTopic(topic: string | Uint8Array): Promise { - return client.getMessage().index(topic) -} - -async function getIotaMilestone(messageId: string): Promise { - const metadata = await client.getMessage().metadata(messageId) - return metadata.referencedByMilestoneIndex -} - -export { sendMessage, receiveMessage, receiveAllMessagesForTopic, getIotaMilestone } +export { sendMessage, receiveMessage } /** * example for message: From e7b3cda2150b873a9a25c6f51879a45ed6bc2c7f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Thu, 25 Jan 2024 13:20:02 +0100 Subject: [PATCH 11/51] change variable name --- dlt-connector/src/manager/InterruptiveSleepManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dlt-connector/src/manager/InterruptiveSleepManager.ts b/dlt-connector/src/manager/InterruptiveSleepManager.ts index 3fc5a9b88..7827c8fe9 100644 --- a/dlt-connector/src/manager/InterruptiveSleepManager.ts +++ b/dlt-connector/src/manager/InterruptiveSleepManager.ts @@ -54,10 +54,10 @@ export class InterruptiveSleepManager { if (!this.interruptiveSleep.has(key)) { this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds)) } - const cond = this.interruptiveSleep.get(key) - if (!cond) { + const interruptiveSleep = this.interruptiveSleep.get(key) + if (!interruptiveSleep) { throw new LogError('map entry not exist after setting it') } - return cond.sleep(ms) + return interruptiveSleep.sleep(ms) } } From 184fe3a2c2aa8664fda758a6344edb6c740b876f Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Thu, 25 Jan 2024 17:19:17 +0100 Subject: [PATCH 12/51] write unit test, small improvements --- dlt-connector/src/config/index.ts | 2 +- dlt-connector/src/data/Mnemonic.ts | 23 ++ dlt-connector/src/data/User.logic.ts | 2 +- dlt-connector/src/index.ts | 4 + .../community/AddCommunity.context.test.ts | 64 +++++ .../CreateTransactionRecipe.context.test.ts | 243 ++++++++++++++++++ .../src/logging/AccountLogging.view.ts | 2 +- 7 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index e6febb482..db26d9f37 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -31,7 +31,7 @@ const database = { const iota = { IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org', 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 = { diff --git a/dlt-connector/src/data/Mnemonic.ts b/dlt-connector/src/data/Mnemonic.ts index 8f15c1046..e23864e60 100644 --- a/dlt-connector/src/data/Mnemonic.ts +++ b/dlt-connector/src/data/Mnemonic.ts @@ -3,10 +3,13 @@ import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39' // eslint-disable-next-line camelcase import { randombytes_buf } from 'sodium-native' +import { LogError } from '@/server/LogError' + export class Mnemonic { private _passphrase = '' public constructor(seed?: Buffer | string) { if (seed) { + Mnemonic.validateSeed(seed) this._passphrase = entropyToMnemonic(seed) return } @@ -22,4 +25,24 @@ export class Mnemonic { public get seed(): Buffer { 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, + }, + ) + } + } } diff --git a/dlt-connector/src/data/User.logic.ts b/dlt-connector/src/data/User.logic.ts index 0a906682d..8bffe326e 100644 --- a/dlt-connector/src/data/User.logic.ts +++ b/dlt-connector/src/data/User.logic.ts @@ -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 */ diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index b998430fd..4e5ef9639 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { CONFIG } from '@/config' +import { Mnemonic } from './data/Mnemonic' import createServer from './server/createServer' import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' async function main() { + if (CONFIG.IOTA_HOME_COMMUNITY_SEED) { + Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED) + } // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) const { app } = await createServer() diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts new file mode 100644 index 000000000..fec2273b6 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts @@ -0,0 +1,64 @@ +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'), + }) + }) +}) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts new file mode 100644 index 000000000..cfd022991 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -0,0 +1,243 @@ +import 'reflect-metadata' +import { Account } from '@entity/Account' +import { Community } from '@entity/Community' +import { TestDB } from '@test/TestDB' +import { Decimal } from 'decimal.js-light' +import { v4 } from 'uuid' + +import { CONFIG } from '@/config' +import { AccountFactory } from '@/data/Account.factory' +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 { UserFactory } from '@/data/User.factory' +import { UserLogic } from '@/data/User.logic' +import { AccountType } from '@/graphql/enum/AccountType' +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view' +import { TransactionLoggingView } from '@/logging/TransactionLogging.view' + +import { AddCommunityContext } from '../community/AddCommunity.context' + +import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285' +const homeCommunityUuid = v4() +const foreignCommunityUuid = v4() + +function createUserIdentifier(userUuid: string, communityUuid: string): UserIdentifier { + const user = new UserIdentifier() + user.uuid = userUuid + user.communityUuid = communityUuid + return user +} + +const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) +const foreignKeyPair = new KeyPair( + new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'), +) +const moderator = createUserIdentifier('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) +const firstUser = createUserIdentifier('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) +const secondUser = createUserIdentifier('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) +const foreignUser = createUserIdentifier( + 'b0155716-5219-4c50-b3d3-0757721ae0d2', + foreignCommunityUuid, +) + +function createUserAndAccount(userIdentifier: UserIdentifier): Account { + const accountDraft = new UserAccountDraft() + accountDraft.user = userIdentifier + accountDraft.createdAt = new Date().toISOString() + accountDraft.accountType = AccountType.COMMUNITY_HUMAN + let _keyPair: KeyPair + if (userIdentifier.communityUuid === homeCommunityUuid) { + _keyPair = keyPair + } else { + _keyPair = foreignKeyPair + } + const user = UserFactory.create(accountDraft, _keyPair) + const userLogic = new UserLogic(user) + const account = AccountFactory.createAccountFromUserAccountDraft( + accountDraft, + userLogic.calculateKeyPair(_keyPair), + ) + account.user = user + return account +} + +describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => { + beforeAll(async () => { + await TestDB.instance.setupTestDB() + const homeCommunityDraft = new CommunityDraft() + homeCommunityDraft.uuid = homeCommunityUuid + homeCommunityDraft.foreign = false + homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z' + let addCommunityContext = new AddCommunityContext(homeCommunityDraft) + await addCommunityContext.run() + + const foreignCommunityDraft = new CommunityDraft() + foreignCommunityDraft.uuid = foreignCommunityUuid + foreignCommunityDraft.foreign = true + foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z' + addCommunityContext = new AddCommunityContext(foreignCommunityDraft) + await addCommunityContext.run() + + const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } }) + // that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain + foreignKeyPair.fillInCommunityKeys(foreignCommunity) + foreignCommunity.save() + + const accounts = [ + createUserAndAccount(moderator), + createUserAndAccount(firstUser), + createUserAndAccount(secondUser), + createUserAndAccount(foreignUser), + ] + await Account.save(accounts) + }) + + 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 + creationTransactionDraft.user = firstUser + 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.signingAccount?.derive2Pubkey.compare( + Buffer.from('19ea7313abc54f120ee0041e5b3b63e34562b0a19b96fa3e6e23cc9bff827a36', 'hex'), + ), + ).toBe(0) + expect( + transaction.recipientAccount?.derive2Pubkey.compare( + Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), + ), + ).toBe(0) + + expect(transaction).toMatchObject({ + type: TransactionType.GRADIDO_CREATION, + protocolVersion: '3.3', + community: { + rootPubkey: Buffer.from( + '07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0', + 'hex', + ), + foreign: 0, + }, + 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( + Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), + ), + ).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 = 1 + sendTransactionDraft.createdAt = new Date().toISOString() + sendTransactionDraft.linkedUser = secondUser + sendTransactionDraft.user = firstUser + sendTransactionDraft.type = InputTransactionType.SEND + sendTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(sendTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('local recv transaction', async () => { + const recvTransactionDraft = new TransactionDraft() + recvTransactionDraft.amount = new Decimal('100') + recvTransactionDraft.backendTransactionId = 1 + recvTransactionDraft.createdAt = new Date().toISOString() + recvTransactionDraft.linkedUser = secondUser + recvTransactionDraft.user = firstUser + recvTransactionDraft.type = InputTransactionType.RECEIVE + recvTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(recvTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('cross group send transaction', async () => { + const crossGroupSendTransactionDraft = new TransactionDraft() + crossGroupSendTransactionDraft.amount = new Decimal('100') + crossGroupSendTransactionDraft.backendTransactionId = 1 + crossGroupSendTransactionDraft.createdAt = new Date().toISOString() + crossGroupSendTransactionDraft.linkedUser = foreignUser + crossGroupSendTransactionDraft.user = firstUser + crossGroupSendTransactionDraft.type = InputTransactionType.SEND + crossGroupSendTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) + + it('cross group recv transaction', async () => { + const crossGroupRecvTransactionDraft = new TransactionDraft() + crossGroupRecvTransactionDraft.amount = new Decimal('100') + crossGroupRecvTransactionDraft.backendTransactionId = 1 + crossGroupRecvTransactionDraft.createdAt = new Date().toISOString() + crossGroupRecvTransactionDraft.linkedUser = foreignUser + crossGroupRecvTransactionDraft.user = firstUser + crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE + crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() + const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft) + await context.run() + const transaction = context.getTransactionRecipe() + const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) + console.log(new TransactionBodyLoggingView(body)) + console.log(new TransactionLoggingView(transaction)) + }) +}) diff --git a/dlt-connector/src/logging/AccountLogging.view.ts b/dlt-connector/src/logging/AccountLogging.view.ts index 76ff7b891..0c97ce469 100644 --- a/dlt-connector/src/logging/AccountLogging.view.ts +++ b/dlt-connector/src/logging/AccountLogging.view.ts @@ -16,7 +16,7 @@ export class AccountLoggingView extends AbstractLoggingView { id: this.account.id, user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null, derivationIndex: this.account.derivationIndex, - derive2pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat), + derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat), type: getEnumValue(AddressType, this.account.type), createdAt: this.dateToString(this.account.createdAt), confirmedAt: this.dateToString(this.account.confirmedAt), From d959fdfbd823d677c06e899f919d7d9eec526671 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 12:23:08 +0100 Subject: [PATCH 13/51] finalize test --- .../CreateTransactionRecipe.context.test.ts | 281 ++++++++++++++---- .../src/logging/TransferAmountLogging.view.ts | 2 +- 2 files changed, 226 insertions(+), 57 deletions(-) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index cfd022991..03474ff28 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,6 +1,7 @@ import 'reflect-metadata' import { Account } from '@entity/Account' import { Community } from '@entity/Community' +import { User } from '@entity/User' import { TestDB } from '@test/TestDB' import { Decimal } from 'decimal.js-light' import { v4 } from 'uuid' @@ -26,6 +27,7 @@ import { TransactionLoggingView } from '@/logging/TransactionLogging.view' import { AddCommunityContext } from '../community/AddCommunity.context' import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' jest.mock('@typeorm/DataSource', () => ({ getDataSource: jest.fn(() => TestDB.instance.dbConnect), @@ -35,6 +37,12 @@ CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa const homeCommunityUuid = v4() const foreignCommunityUuid = v4() +type UserSet = { + identifier: UserIdentifier + user: User + account: Account +} + function createUserIdentifier(userUuid: string, communityUuid: string): UserIdentifier { const user = new UserIdentifier() user.uuid = userUuid @@ -46,13 +54,6 @@ const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) const foreignKeyPair = new KeyPair( new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'), ) -const moderator = createUserIdentifier('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) -const firstUser = createUserIdentifier('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) -const secondUser = createUserIdentifier('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) -const foreignUser = createUserIdentifier( - 'b0155716-5219-4c50-b3d3-0757721ae0d2', - foreignCommunityUuid, -) function createUserAndAccount(userIdentifier: UserIdentifier): Account { const accountDraft = new UserAccountDraft() @@ -75,6 +76,27 @@ function createUserAndAccount(userIdentifier: UserIdentifier): Account { return account } +function createUserSet(userUuid: string, communityUuid: string): UserSet { + const identifier = createUserIdentifier(userUuid, communityUuid) + const account = createUserAndAccount(identifier) + if (!account.user) { + throw Error('user missing') + } + return { + identifier, + account, + user: account.user, + } +} + +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() @@ -97,13 +119,17 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context foreignKeyPair.fillInCommunityKeys(foreignCommunity) foreignCommunity.save() - const accounts = [ - createUserAndAccount(moderator), - createUserAndAccount(firstUser), - createUserAndAccount(secondUser), - createUserAndAccount(foreignUser), - ] - await Account.save(accounts) + moderator = createUserSet('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) + firstUser = createUserSet('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) + secondUser = createUserSet('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) + foreignUser = createUserSet('b0155716-5219-4c50-b3d3-0757721ae0d2', foreignCommunityUuid) + + await Account.save([ + moderator.account, + firstUser.account, + secondUser.account, + foreignUser.account, + ]) }) afterAll(async () => { @@ -115,8 +141,8 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context creationTransactionDraft.amount = new Decimal('2000') creationTransactionDraft.backendTransactionId = 1 creationTransactionDraft.createdAt = new Date().toISOString() - creationTransactionDraft.linkedUser = moderator - creationTransactionDraft.user = firstUser + creationTransactionDraft.linkedUser = moderator.identifier + creationTransactionDraft.user = firstUser.identifier creationTransactionDraft.type = InputTransactionType.CREATION creationTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(creationTransactionDraft) @@ -124,26 +150,19 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context const transaction = context.getTransactionRecipe() // console.log(new TransactionLoggingView(transaction)) - expect( - transaction.signingAccount?.derive2Pubkey.compare( - Buffer.from('19ea7313abc54f120ee0041e5b3b63e34562b0a19b96fa3e6e23cc9bff827a36', 'hex'), - ), - ).toBe(0) - expect( - transaction.recipientAccount?.derive2Pubkey.compare( - Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), - ), - ).toBe(0) - expect(transaction).toMatchObject({ type: TransactionType.GRADIDO_CREATION, protocolVersion: '3.3', community: { - rootPubkey: Buffer.from( - '07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0', - 'hex', - ), + rootPubkey: keyPair.publicKey, foreign: 0, + iotaTopic: topic, + }, + signingAccount: { + derive2Pubkey: moderator.account.derive2Pubkey, + }, + recipientAccount: { + derive2Pubkey: firstUser.account.derive2Pubkey, }, amount: new Decimal(2000), backendTransactions: [ @@ -158,11 +177,8 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context expect(body.creation).toBeDefined() if (!body.creation) throw new Error() const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey) - expect( - bodyReceiverPubkey.compare( - Buffer.from('5875e1a5e101301cc774b7462566ec2d1a0b04a091dab2e32cecd713b3346224', 'hex'), - ), - ).toBe(0) + expect(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0) + expect(body).toMatchObject({ type: CrossGroupType.LOCAL, creation: { @@ -176,68 +192,221 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context it('local send transaction', async () => { const sendTransactionDraft = new TransactionDraft() sendTransactionDraft.amount = new Decimal('100') - sendTransactionDraft.backendTransactionId = 1 + sendTransactionDraft.backendTransactionId = 2 sendTransactionDraft.createdAt = new Date().toISOString() - sendTransactionDraft.linkedUser = secondUser - sendTransactionDraft.user = firstUser + sendTransactionDraft.linkedUser = secondUser.identifier + sendTransactionDraft.user = firstUser.identifier sendTransactionDraft.type = InputTransactionType.SEND sendTransactionDraft.targetDate = new Date().toISOString() 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)) - console.log(new TransactionLoggingView(transaction)) + // 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 = 1 + recvTransactionDraft.backendTransactionId = 3 recvTransactionDraft.createdAt = new Date().toISOString() - recvTransactionDraft.linkedUser = secondUser - recvTransactionDraft.user = firstUser + recvTransactionDraft.linkedUser = firstUser.identifier + recvTransactionDraft.user = secondUser.identifier recvTransactionDraft.type = InputTransactionType.RECEIVE recvTransactionDraft.targetDate = new Date().toISOString() 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)) - console.log(new TransactionLoggingView(transaction)) + // 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 = 1 + crossGroupSendTransactionDraft.backendTransactionId = 4 crossGroupSendTransactionDraft.createdAt = new Date().toISOString() - crossGroupSendTransactionDraft.linkedUser = foreignUser - crossGroupSendTransactionDraft.user = firstUser + crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier + crossGroupSendTransactionDraft.user = firstUser.identifier crossGroupSendTransactionDraft.type = InputTransactionType.SEND crossGroupSendTransactionDraft.targetDate = new Date().toISOString() 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)) - console.log(new TransactionLoggingView(transaction)) + // 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, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) it('cross group recv transaction', async () => { const crossGroupRecvTransactionDraft = new TransactionDraft() crossGroupRecvTransactionDraft.amount = new Decimal('100') - crossGroupRecvTransactionDraft.backendTransactionId = 1 + crossGroupRecvTransactionDraft.backendTransactionId = 5 crossGroupRecvTransactionDraft.createdAt = new Date().toISOString() - crossGroupRecvTransactionDraft.linkedUser = foreignUser - crossGroupRecvTransactionDraft.user = firstUser + crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier + crossGroupRecvTransactionDraft.user = foreignUser.identifier crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() 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: 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.RECEIVE, + }, + ], + }) const body = TransactionBody.fromBodyBytes(transaction.bodyBytes) - console.log(new TransactionBodyLoggingView(body)) - console.log(new TransactionLoggingView(transaction)) + // 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, + transfer: { + sender: { + amount: '100', + }, + }, + }) }) }) diff --git a/dlt-connector/src/logging/TransferAmountLogging.view.ts b/dlt-connector/src/logging/TransferAmountLogging.view.ts index 8d320b99f..2384bfdb4 100644 --- a/dlt-connector/src/logging/TransferAmountLogging.view.ts +++ b/dlt-connector/src/logging/TransferAmountLogging.view.ts @@ -10,7 +10,7 @@ export class TransferAmountLoggingView extends AbstractLoggingView { // eslint-disable-next-line @typescript-eslint/no-explicit-any public toJSON(): any { return { - publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat), + pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat), amount: this.self.amount, communityId: this.self.communityId, } From 8a135bd9835dd65590b8bf4a1024643c3914a989 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 15:42:14 +0100 Subject: [PATCH 14/51] add test for transmitToIota context, move some test init code into seeding --- dlt-connector/jest.config.js | 2 +- dlt-connector/src/data/Account.logic.ts | 35 ++++ dlt-connector/src/data/Transaction.logic.ts | 23 ++- .../src/graphql/input/CommunityDraft.ts | 3 +- .../CreateTransactionRecipe.context.test.ts | 99 ++--------- .../transaction/TransactionRecipe.role.ts | 5 +- .../AbstractTransactionRecipe.role.ts | 8 +- .../TransmitToIota.context.test.ts | 167 ++++++++++++++++++ .../transmitToIota/TransmitToIota.context.ts | 17 +- .../src/logging/TransactionLogging.view.ts | 15 +- dlt-connector/test/seeding/Community.seed.ts | 28 +++ dlt-connector/test/seeding/UserSet.seed.ts | 55 ++++++ 12 files changed, 355 insertions(+), 102 deletions(-) create mode 100644 dlt-connector/src/data/Account.logic.ts create mode 100644 dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts create mode 100644 dlt-connector/test/seeding/Community.seed.ts create mode 100644 dlt-connector/test/seeding/UserSet.seed.ts diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 69bc64bb2..2de18cf50 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 66, + lines: 71, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/src/data/Account.logic.ts b/dlt-connector/src/data/Account.logic.ts new file mode 100644 index 000000000..9cff66070 --- /dev/null +++ b/dlt-connector/src/data/Account.logic.ts @@ -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 + } +} diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index 62f7c3732..9ca6330ba 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -77,14 +77,14 @@ export class TransactionLogic { 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.communityId || this.self.otherCommunityId !== otherTransaction.otherCommunityId || - this.self.amount !== otherTransaction.amount || this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || - this.self.createdAt !== otherTransaction.createdAt + this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime() ) { logger.debug('transaction a and b are not pairs', { a: new TransactionLoggingView(this.self), @@ -135,6 +135,25 @@ export class TransactionLogic { logger.info(`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), diff --git a/dlt-connector/src/graphql/input/CommunityDraft.ts b/dlt-connector/src/graphql/input/CommunityDraft.ts index 665e10b75..0b26e68d0 100644 --- a/dlt-connector/src/graphql/input/CommunityDraft.ts +++ b/dlt-connector/src/graphql/input/CommunityDraft.ts @@ -1,10 +1,9 @@ // https://www.npmjs.com/package/@apollo/protobufjs +import { isValidDateString } from '@validator/DateString' import { IsBoolean, IsUUID } from 'class-validator' import { Field, InputType } from 'type-graphql' -import { isValidDateString } from '@validator/DateString' - @InputType() export class CommunityDraft { @Field(() => String) diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 03474ff28..9ddbebd06 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,33 +1,25 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { Community } from '@entity/Community' -import { User } from '@entity/User' import { TestDB } from '@test/TestDB' import { Decimal } from 'decimal.js-light' import { v4 } from 'uuid' import { CONFIG } from '@/config' -import { AccountFactory } from '@/data/Account.factory' 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 { UserFactory } from '@/data/User.factory' -import { UserLogic } from '@/data/User.logic' -import { AccountType } from '@/graphql/enum/AccountType' import { InputTransactionType } from '@/graphql/enum/InputTransactionType' -import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' -import { UserIdentifier } from '@/graphql/input/UserIdentifier' -import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view' -import { TransactionLoggingView } from '@/logging/TransactionLogging.view' - -import { AddCommunityContext } from '../community/AddCommunity.context' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' import { CreateTransactionRecipeContext } from './CreateTransationRecipe.context' -import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +// 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), @@ -37,58 +29,11 @@ CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa const homeCommunityUuid = v4() const foreignCommunityUuid = v4() -type UserSet = { - identifier: UserIdentifier - user: User - account: Account -} - -function createUserIdentifier(userUuid: string, communityUuid: string): UserIdentifier { - const user = new UserIdentifier() - user.uuid = userUuid - user.communityUuid = communityUuid - return user -} - const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) const foreignKeyPair = new KeyPair( new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'), ) -function createUserAndAccount(userIdentifier: UserIdentifier): Account { - const accountDraft = new UserAccountDraft() - accountDraft.user = userIdentifier - accountDraft.createdAt = new Date().toISOString() - accountDraft.accountType = AccountType.COMMUNITY_HUMAN - let _keyPair: KeyPair - if (userIdentifier.communityUuid === homeCommunityUuid) { - _keyPair = keyPair - } else { - _keyPair = foreignKeyPair - } - const user = UserFactory.create(accountDraft, _keyPair) - const userLogic = new UserLogic(user) - const account = AccountFactory.createAccountFromUserAccountDraft( - accountDraft, - userLogic.calculateKeyPair(_keyPair), - ) - account.user = user - return account -} - -function createUserSet(userUuid: string, communityUuid: string): UserSet { - const identifier = createUserIdentifier(userUuid, communityUuid) - const account = createUserAndAccount(identifier) - if (!account.user) { - throw Error('user missing') - } - return { - identifier, - account, - user: account.user, - } -} - let moderator: UserSet let firstUser: UserSet let secondUser: UserSet @@ -100,29 +45,13 @@ const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid) describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => { beforeAll(async () => { await TestDB.instance.setupTestDB() - const homeCommunityDraft = new CommunityDraft() - homeCommunityDraft.uuid = homeCommunityUuid - homeCommunityDraft.foreign = false - homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z' - let addCommunityContext = new AddCommunityContext(homeCommunityDraft) - await addCommunityContext.run() + await communitySeed(homeCommunityUuid, false) + await communitySeed(foreignCommunityUuid, true, foreignKeyPair) - const foreignCommunityDraft = new CommunityDraft() - foreignCommunityDraft.uuid = foreignCommunityUuid - foreignCommunityDraft.foreign = true - foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z' - addCommunityContext = new AddCommunityContext(foreignCommunityDraft) - await addCommunityContext.run() - - const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } }) - // that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain - foreignKeyPair.fillInCommunityKeys(foreignCommunity) - foreignCommunity.save() - - moderator = createUserSet('ff8bbdcb-fc8b-4b5d-98e3-8bd7e1afcdbb', homeCommunityUuid) - firstUser = createUserSet('8e47e32e-0182-4099-b94d-0cac567d1392', homeCommunityUuid) - secondUser = createUserSet('9c8611dd-ee93-4cdb-a600-396c2ca91cc7', homeCommunityUuid) - foreignUser = createUserSet('b0155716-5219-4c50-b3d3-0757721ae0d2', foreignCommunityUuid) + moderator = createUserSet(homeCommunityUuid, keyPair) + firstUser = createUserSet(homeCommunityUuid, keyPair) + secondUser = createUserSet(homeCommunityUuid, keyPair) + foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair) await Account.save([ moderator.account, @@ -197,7 +126,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context sendTransactionDraft.linkedUser = secondUser.identifier sendTransactionDraft.user = firstUser.identifier sendTransactionDraft.type = InputTransactionType.SEND - sendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(sendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -251,7 +179,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context recvTransactionDraft.linkedUser = firstUser.identifier recvTransactionDraft.user = secondUser.identifier recvTransactionDraft.type = InputTransactionType.RECEIVE - recvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(recvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -304,7 +231,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier crossGroupSendTransactionDraft.user = firstUser.identifier crossGroupSendTransactionDraft.type = InputTransactionType.SEND - crossGroupSendTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() @@ -361,7 +287,6 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier crossGroupRecvTransactionDraft.user = foreignUser.identifier crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE - crossGroupRecvTransactionDraft.targetDate = new Date().toISOString() const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft) await context.run() const transaction = context.getTransactionRecipe() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index d36aa98cc..7c6d3015d 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -1,5 +1,6 @@ import { Transaction } from '@entity/Transaction' +import { AccountLogic } from '@/data/Account.logic' import { KeyPair } from '@/data/KeyPair' import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' import { TransactionBuilder } from '@/data/Transaction.builder' @@ -57,9 +58,11 @@ export class TransactionRecipeRole { await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser) } const transaction = this.transactionBuilder.getTransaction() + const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity()) + const accountLogic = new AccountLogic(signingAccount) // sign this.transactionBuilder.setSignature( - new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes), + accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes), ) return this } diff --git a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts index 21285705e..23fd9d275 100644 --- a/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/transmitToIota/AbstractTransactionRecipe.role.ts @@ -11,14 +11,14 @@ import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLoggi import { logger } from '@/logging/logger' export abstract class AbstractTransactionRecipeRole { - protected transactionBody: TransactionBody | undefined - // eslint-disable-next-line no-useless-constructor - public constructor(protected self: Transaction) {} + protected transactionBody: TransactionBody + public constructor(protected self: Transaction) { + this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) + } public abstract transmitToIota(): Promise protected getGradidoTransaction(): GradidoTransaction { - this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes) const transaction = new GradidoTransaction(this.transactionBody) if (!this.self.signature) { throw new TransactionError( diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts new file mode 100644 index 000000000..8e1ba4f33 --- /dev/null +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts @@ -0,0 +1,167 @@ +import 'reflect-metadata' +import { Account } from '@entity/Account' +import { TestDB } from '@test/TestDB' +import { Decimal } from 'decimal.js-light' +import { v4 } from 'uuid' + +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({}), + ) + }) +}) diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts index 8e0b32c6c..f29d1742a 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -2,7 +2,10 @@ 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' @@ -36,7 +39,19 @@ export class TransmitToIotaContext { public async run(): Promise { const transaction = await this.transactionRecipeRole.transmitToIota() + logger.debug('transaction sended via iota', new TransactionLoggingView(transaction)) // store changes in db - await transaction.save() + // prevent endless loop + const paringTransaction = transaction.paringTransaction + if (paringTransaction) { + transaction.paringTransaction = undefined + await getDataSource().transaction(async (transactionalEntityManager) => { + await transactionalEntityManager.save(transaction) + await transactionalEntityManager.save(paringTransaction) + }) + } else { + await transaction.save() + } + logger.info('sended transaction successfully updated in db') } } diff --git a/dlt-connector/src/logging/TransactionLogging.view.ts b/dlt-connector/src/logging/TransactionLogging.view.ts index 38443024d..d04d46556 100644 --- a/dlt-connector/src/logging/TransactionLogging.view.ts +++ b/dlt-connector/src/logging/TransactionLogging.view.ts @@ -18,7 +18,7 @@ export class TransactionLoggingView extends AbstractLoggingView { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public toJSON(showBackendTransactions = true): any { + public toJSON(showBackendTransactions = true, deep = 1): any { return { id: this.self.id, nr: this.self.nr, @@ -31,16 +31,23 @@ export class TransactionLoggingView extends AbstractLoggingView { community: new CommunityLoggingView(this.self.community).toJSON(), otherCommunity: this.self.otherCommunity ? new CommunityLoggingView(this.self.otherCommunity) - : undefined, + : { id: this.self.otherCommunityId }, iotaMessageId: this.self.iotaMessageId ? this.self.iotaMessageId.toString(this.bufferStringFormat) : undefined, signingAccount: this.self.signingAccount ? new AccountLoggingView(this.self.signingAccount) - : undefined, + : { id: this.self.signingAccountId }, recipientAccount: this.self.recipientAccount ? new AccountLoggingView(this.self.recipientAccount) - : undefined, + : { id: this.self.recipientAccountId }, + pairingTransaction: + this.self.paringTransaction && deep === 1 + ? new TransactionLoggingView(this.self.paringTransaction).toJSON( + showBackendTransactions, + deep + 1, + ) + : { id: this.self.paringTransactionId }, amount: this.decimalToString(this.self.amount), accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation), accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation), diff --git a/dlt-connector/test/seeding/Community.seed.ts b/dlt-connector/test/seeding/Community.seed.ts new file mode 100644 index 000000000..a1b042ef2 --- /dev/null +++ b/dlt-connector/test/seeding/Community.seed.ts @@ -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 => { + 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 +} diff --git a/dlt-connector/test/seeding/UserSet.seed.ts b/dlt-connector/test/seeding/UserSet.seed.ts new file mode 100644 index 000000000..933b386ca --- /dev/null +++ b/dlt-connector/test/seeding/UserSet.seed.ts @@ -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, + } +} From 8445c0bec27e3df27838b56ecb7aea04cc16f954 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 17:23:09 +0100 Subject: [PATCH 15/51] test paring transaction validation --- .../src/data/Transaction.logic.test.ts | 323 ++++++++++++++++++ dlt-connector/src/data/Transaction.logic.ts | 62 ++-- 2 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 dlt-connector/src/data/Transaction.logic.test.ts diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts new file mode 100644 index 000000000..c652fe794 --- /dev/null +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -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 { 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' +import { GradidoCreation } from './proto/3_3/GradidoCreation' +import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer' + +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 = 1 + b.otherCommunityId = 2 + 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({})) + }) + }) +}) diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index 9ca6330ba..f237e9b3b 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -83,30 +83,26 @@ export class TransactionLogic { this.self.recipientAccountId !== otherTransaction.recipientAccountId || this.self.communityId !== otherTransaction.communityId || this.self.otherCommunityId !== otherTransaction.otherCommunityId || - this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation || this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime() ) { - logger.debug('transaction a and b are not pairs', { - a: new TransactionLoggingView(this.self), - b: new TransactionLoggingView(otherTransaction), + 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 can be Cross + * both must be Cross or * one can be OUTBOUND and one can be INBOUND * no one can be LOCAL */ - if ( - (body.type === otherBody.type && body.type !== CrossGroupType.CROSS) || - body.type === CrossGroupType.LOCAL || - otherBody.type === CrossGroupType.LOCAL - ) { + + if (!this.validCrossGroupTypes(body.type, otherBody.type)) { logger.info("cross group types don't match", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } @@ -114,14 +110,14 @@ export class TransactionLogic { const otherType = otherBody.getTransactionType() if (!type || !otherType) { throw new LogError("couldn't determine transaction type", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) } if (type !== otherType) { logger.info("transaction types don't match", { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + a: new TransactionBodyLoggingView(body).toJSON(), + b: new TransactionBodyLoggingView(otherBody).toJSON(), }) return false } @@ -132,7 +128,7 @@ export class TransactionLogic { TransactionType.GRADIDO_DEFERRED_TRANSFER, ].includes(type) ) { - logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`) + logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`) return false } if ( @@ -156,21 +152,45 @@ export class TransactionLogic { } if (body.otherGroup === otherBody.otherGroup) { logger.info('otherGroups are the same', { - a: new TransactionBodyLoggingView(body), - b: new TransactionBodyLoggingView(otherBody), + 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), - b: new TransactionBodyLoggingView(otherBody), + 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) From 23843bdd14f9413cecbd19190f8852bac05f97a2 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Sat, 27 Jan 2024 17:43:14 +0100 Subject: [PATCH 16/51] lint --- dlt-connector/jest.config.js | 2 +- dlt-connector/package.json | 3 +- .../src/data/Transaction.logic.test.ts | 4 +-- .../src/graphql/input/CommunityDraft.ts | 3 +- .../community/AddCommunity.context.test.ts | 1 + .../CreateTransactionRecipe.context.test.ts | 3 +- .../TransmitToIota.context.test.ts | 3 +- dlt-connector/src/utils/typeConverter.test.ts | 36 +++++++++++++++++-- dlt-connector/yarn.lock | 2 +- 9 files changed, 47 insertions(+), 10 deletions(-) diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 2de18cf50..3d731787f 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 71, + lines: 75, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 8b5ae357c..7c8644150 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -39,7 +39,8 @@ "reflect-metadata": "^0.1.13", "sodium-native": "^4.0.4", "tsconfig-paths": "^4.1.2", - "type-graphql": "^2.0.0-beta.2" + "type-graphql": "^2.0.0-beta.2", + "uuid": "^9.0.1" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts index c652fe794..fe9c2c085 100644 --- a/dlt-connector/src/data/Transaction.logic.test.ts +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -6,12 +6,12 @@ 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' -import { GradidoCreation } from './proto/3_3/GradidoCreation' -import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer' let a: Transaction let b: Transaction diff --git a/dlt-connector/src/graphql/input/CommunityDraft.ts b/dlt-connector/src/graphql/input/CommunityDraft.ts index 0b26e68d0..665e10b75 100644 --- a/dlt-connector/src/graphql/input/CommunityDraft.ts +++ b/dlt-connector/src/graphql/input/CommunityDraft.ts @@ -1,9 +1,10 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { isValidDateString } from '@validator/DateString' import { IsBoolean, IsUUID } from 'class-validator' import { Field, InputType } from 'type-graphql' +import { isValidDateString } from '@validator/DateString' + @InputType() export class CommunityDraft { @Field(() => String) diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts index fec2273b6..d7ec4e9c6 100644 --- a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.test.ts @@ -1,5 +1,6 @@ import 'reflect-metadata' import { Community } from '@entity/Community' + import { TestDB } from '@test/TestDB' import { CONFIG } from '@/config' diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 9ddbebd06..0382632b3 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -1,9 +1,10 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { TestDB } from '@test/TestDB' 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' diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts index 8e1ba4f33..94a8e4f9d 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.test.ts @@ -1,9 +1,10 @@ import 'reflect-metadata' import { Account } from '@entity/Account' -import { TestDB } from '@test/TestDB' 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' diff --git a/dlt-connector/src/utils/typeConverter.test.ts b/dlt-connector/src/utils/typeConverter.test.ts index d9b1c2356..4caee94bb 100644 --- a/dlt-connector/src/utils/typeConverter.test.ts +++ b/dlt-connector/src/utils/typeConverter.test.ts @@ -1,13 +1,45 @@ import 'reflect-metadata' + import { Timestamp } from '@/data/proto/3_3/Timestamp' -import { timestampToDate } from './typeConverter' +import { + base64ToBuffer, + iotaTopicFromCommunityUUID, + timestampSecondsToDate, + timestampToDate, + uuid4ToBuffer, +} from './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', () => { - 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) expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000)) 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'), + ) + }) }) diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 6d50426b1..7f46d88bc 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -6388,7 +6388,7 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== From 95b4810631c72b2e25c16eaff4c938deb16c24e0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 16:30:07 +0100 Subject: [PATCH 17/51] change logic, transaction.community is now community on which blockchain it is, not longer signerCommunity, transaction.otherCommunity is now the same like TransactionBody.otherGroup --- dlt-connector/src/data/Transaction.builder.ts | 16 +++++----- .../src/data/Transaction.logic.test.ts | 4 +-- dlt-connector/src/data/Transaction.logic.ts | 4 +-- .../src/graphql/input/TransactionDraft.ts | 7 ++--- .../src/graphql/input/UserIdentifier.ts | 4 +-- .../transaction/AbstractTransaction.role.ts | 9 ++++-- .../CreateTransactionRecipe.context.test.ts | 12 ++++---- .../transaction/CreationTransaction.role.ts | 29 +++++++++++++++++++ .../transaction/TransactionRecipe.role.ts | 22 ++++++++++++-- dlt-connector/tsconfig.json | 2 +- 10 files changed, 79 insertions(+), 30 deletions(-) diff --git a/dlt-connector/src/data/Transaction.builder.ts b/dlt-connector/src/data/Transaction.builder.ts index 115391e91..f46f02a29 100644 --- a/dlt-connector/src/data/Transaction.builder.ts +++ b/dlt-connector/src/data/Transaction.builder.ts @@ -59,6 +59,10 @@ export class TransactionBuilder { return this.transaction.community } + public getOtherCommunity(): Community | undefined { + return this.transaction.otherCommunity + } + public setSigningAccount(signingAccount: Account): TransactionBuilder { this.transaction.signingAccount = signingAccount return this @@ -103,22 +107,18 @@ export class TransactionBuilder { return this } - public async setSenderCommunityFromSenderUser( - senderUser: UserIdentifier, - ): Promise { + public async setCommunityFromUser(user: UserIdentifier): Promise { // get sender community - const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser) + const community = await CommunityRepository.getCommunityForUserIdentifier(user) if (!community) { throw new LogError("couldn't find community for transaction") } return this.setCommunity(community) } - public async setOtherCommunityFromRecipientUser( - recipientUser: UserIdentifier, - ): Promise { + public async setOtherCommunityFromUser(user: UserIdentifier): Promise { // get recipient community - const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser) + const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user) return this.setOtherCommunity(otherCommunity) } diff --git a/dlt-connector/src/data/Transaction.logic.test.ts b/dlt-connector/src/data/Transaction.logic.test.ts index fe9c2c085..b5ef73de2 100644 --- a/dlt-connector/src/data/Transaction.logic.test.ts +++ b/dlt-connector/src/data/Transaction.logic.test.ts @@ -44,8 +44,8 @@ describe('data/transaction.logic', () => { b = new Transaction() b.community = new Community() - b.communityId = 1 - b.otherCommunityId = 2 + b.communityId = 2 + b.otherCommunityId = 1 b.id = 2 b.signingAccountId = 1 b.recipientAccountId = 2 diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index f237e9b3b..f61c9a6e9 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -81,8 +81,8 @@ export class TransactionLogic { if ( this.self.signingAccountId !== otherTransaction.signingAccountId || this.self.recipientAccountId !== otherTransaction.recipientAccountId || - this.self.communityId !== otherTransaction.communityId || - this.self.otherCommunityId !== otherTransaction.otherCommunityId || + 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', { diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 541797565..c69a7dea3 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,11 +1,10 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' -import { Decimal } from 'decimal.js-light' -import { InputType, Field, Int } from 'type-graphql' - import { InputTransactionType } from '@enum/InputTransactionType' import { isValidDateString } from '@validator/DateString' import { IsPositiveDecimal } from '@validator/Decimal' +import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' +import { Decimal } from 'decimal.js-light' +import { InputType, Field, Int } from 'type-graphql' import { UserIdentifier } from './UserIdentifier' diff --git a/dlt-connector/src/graphql/input/UserIdentifier.ts b/dlt-connector/src/graphql/input/UserIdentifier.ts index 12f2e5889..7d9035b93 100644 --- a/dlt-connector/src/graphql/input/UserIdentifier.ts +++ b/dlt-connector/src/graphql/input/UserIdentifier.ts @@ -9,9 +9,9 @@ export class UserIdentifier { @IsUUID('4') uuid: string - @Field(() => String, { nullable: true }) + @Field(() => String) @IsUUID('4') - communityUuid?: string + communityUuid: string @Field(() => Int, { defaultValue: 1, nullable: true }) @IsPositive() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts index 62fcf90de..b23d381cd 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts @@ -1,8 +1,11 @@ +import { Community } from '@entity/Community' + import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' import { UserIdentifier } from '@/graphql/input/UserIdentifier' import { TransactionError } from '@/graphql/model/TransactionError' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' export abstract class AbstractTransactionRole { // eslint-disable-next-line no-useless-constructor @@ -26,7 +29,7 @@ export abstract class AbstractTransactionRole { * OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2' * INBOUND: goes to receiver, stored on receiver community blockchain * INBOUND: stored on 'gdd2', otherGroup: 'gdd1' - * @returns + * @returns iota topic */ public getOtherGroup(): string { let user: UserIdentifier @@ -42,7 +45,7 @@ export abstract class AbstractTransactionRole { 'missing sender/signing user community id for cross group transaction', ) } - return user.communityUuid + return iotaTopicFromCommunityUUID(user.communityUuid) case CrossGroupType.OUTBOUND: user = this.getRecipientUser() if (!user.communityUuid) { @@ -51,7 +54,7 @@ export abstract class AbstractTransactionRole { 'missing recipient user community id for cross group transaction', ) } - return user.communityUuid + return iotaTopicFromCommunityUUID(user.communityUuid) default: throw new TransactionError( TransactionErrorType.NOT_IMPLEMENTED_YET, diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts index 0382632b3..e5535f3f7 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransactionRecipe.context.test.ts @@ -272,6 +272,7 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context ) expect(body).toMatchObject({ type: CrossGroupType.OUTBOUND, + otherGroup: foreignTopic, transfer: { sender: { amount: '100', @@ -296,15 +297,15 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context type: TransactionType.GRADIDO_TRANSFER, protocolVersion: '3.3', community: { - rootPubkey: keyPair.publicKey, - foreign: 0, - iotaTopic: topic, - }, - otherCommunity: { rootPubkey: foreignKeyPair.publicKey, foreign: 1, iotaTopic: foreignTopic, }, + otherCommunity: { + rootPubkey: keyPair.publicKey, + foreign: 0, + iotaTopic: topic, + }, signingAccount: { derive2Pubkey: firstUser.account.derive2Pubkey, }, @@ -328,6 +329,7 @@ describe('interactions/backendToDb/transaction/Create Transaction Recipe Context ) expect(body).toMatchObject({ type: CrossGroupType.INBOUND, + otherGroup: topic, transfer: { sender: { amount: '100', diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts index 7b82f8805..f11518d02 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts @@ -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 { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' 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' @@ -15,4 +22,26 @@ export class CreationTransactionRole extends AbstractTransactionRole { public getCrossGroupType(): CrossGroupType { return CrossGroupType.LOCAL } + + public async getCommunity(): Promise { + 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 { + return null + } } diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts index 7c6d3015d..f1be50c75 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -1,7 +1,9 @@ +import { Community } from '@entity/Community' import { Transaction } from '@entity/Transaction' import { AccountLogic } from '@/data/Account.logic' import { KeyPair } from '@/data/KeyPair' +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' import { TransactionBuilder } from '@/data/Transaction.builder' import { UserRepository } from '@/data/User.repository' @@ -53,12 +55,15 @@ export class TransactionRecipeRole { this.transactionBuilder .fromTransactionBodyBuilder(transactionBodyBuilder) .addBackendTransaction(transactionDraft) - await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser) + + await this.transactionBuilder.setCommunityFromUser(transactionDraft.user) if (recipientUser.communityUuid !== signingUser.communityUuid) { - await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser) + await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser) } const transaction = this.transactionBuilder.getTransaction() - const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity()) + const communityKeyPair = new KeyPair( + this.getSigningCommunity(transactionTypeRole.getCrossGroupType()), + ) const accountLogic = new AccountLogic(signingAccount) // sign this.transactionBuilder.setSignature( @@ -67,6 +72,17 @@ export class TransactionRecipeRole { 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 { return this.transactionBuilder.getTransaction() } diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index e37b2a7a0..32525c013 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -63,7 +63,7 @@ "@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["node_modules/@types", "@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ From b2f77ddfbf01982357364e784701dec812972f4e Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 16:43:19 +0100 Subject: [PATCH 18/51] fix linting --- dlt-connector/src/graphql/input/TransactionDraft.ts | 7 ++++--- .../backendToDb/transaction/AbstractTransaction.role.ts | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index c69a7dea3..541797565 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,11 +1,12 @@ // https://www.npmjs.com/package/@apollo/protobufjs -import { InputTransactionType } from '@enum/InputTransactionType' -import { isValidDateString } from '@validator/DateString' -import { IsPositiveDecimal } from '@validator/Decimal' import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' import { Decimal } from 'decimal.js-light' import { InputType, Field, Int } from 'type-graphql' +import { InputTransactionType } from '@enum/InputTransactionType' +import { isValidDateString } from '@validator/DateString' +import { IsPositiveDecimal } from '@validator/Decimal' + import { UserIdentifier } from './UserIdentifier' @InputType() diff --git a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts index b23d381cd..89bdbbedf 100644 --- a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts +++ b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts @@ -1,5 +1,3 @@ -import { Community } from '@entity/Community' - import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { TransactionDraft } from '@/graphql/input/TransactionDraft' From 07d2e6c846fccd94e696af5363e1d5b2846e8837 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 3 Feb 2024 18:54:42 +0100 Subject: [PATCH 19/51] rename paringTransaction to pairingTransaction --- dlt-connector/src/data/Transaction.logic.ts | 10 +- .../InboundTransactionRecipe.role.ts | 6 +- .../transmitToIota/TransmitToIota.context.ts | 8 +- .../src/logging/TransactionLogging.view.ts | 6 +- .../entity/0004-fix_spelling/Transaction.ts | 128 ++++++++++++++++++ dlt-database/entity/Transaction.ts | 2 +- dlt-database/migrations/0004-fix_spelling.ts | 15 ++ 7 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 dlt-database/entity/0004-fix_spelling/Transaction.ts create mode 100644 dlt-database/migrations/0004-fix_spelling.ts diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index f61c9a6e9..ec13bf753 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -26,13 +26,13 @@ export class TransactionLogic { throw new LogError("local transaction don't has a pairing transaction") } - // check if already on entity - if (this.self.paringTransaction) { - return this.self.paringTransaction + // check if already was loaded from db + if (this.self.pairingTransaction) { + return this.self.pairingTransaction } - if (this.self.paringTransactionId) { - const pairingTransaction = await Transaction.findOneBy({ id: this.self.paringTransactionId }) + if (this.self.pairingTransaction) { + const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction }) if (pairingTransaction) { return pairingTransaction } diff --git a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts index 52e95c12c..2f18b48ac 100644 --- a/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts +++ b/dlt-connector/src/interactions/transmitToIota/InboundTransactionRecipe.role.ts @@ -23,9 +23,9 @@ export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole ) } gradidoTransaction.parentMessageId = pairingTransaction.iotaMessageId - this.self.paringTransactionId = pairingTransaction.id - this.self.paringTransaction = pairingTransaction - pairingTransaction.paringTransactionId = this.self.id + this.self.pairingTransactionId = pairingTransaction.id + this.self.pairingTransaction = pairingTransaction + pairingTransaction.pairingTransactionId = this.self.id if (!this.self.otherCommunity) { throw new LogError('missing other community') diff --git a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts index f29d1742a..e58cd7e89 100644 --- a/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts +++ b/dlt-connector/src/interactions/transmitToIota/TransmitToIota.context.ts @@ -42,12 +42,12 @@ export class TransmitToIotaContext { logger.debug('transaction sended via iota', new TransactionLoggingView(transaction)) // store changes in db // prevent endless loop - const paringTransaction = transaction.paringTransaction - if (paringTransaction) { - transaction.paringTransaction = undefined + const pairingTransaction = transaction.pairingTransaction + if (pairingTransaction) { + transaction.pairingTransaction = undefined await getDataSource().transaction(async (transactionalEntityManager) => { await transactionalEntityManager.save(transaction) - await transactionalEntityManager.save(paringTransaction) + await transactionalEntityManager.save(pairingTransaction) }) } else { await transaction.save() diff --git a/dlt-connector/src/logging/TransactionLogging.view.ts b/dlt-connector/src/logging/TransactionLogging.view.ts index d04d46556..1bb59cc55 100644 --- a/dlt-connector/src/logging/TransactionLogging.view.ts +++ b/dlt-connector/src/logging/TransactionLogging.view.ts @@ -42,12 +42,12 @@ export class TransactionLoggingView extends AbstractLoggingView { ? new AccountLoggingView(this.self.recipientAccount) : { id: this.self.recipientAccountId }, pairingTransaction: - this.self.paringTransaction && deep === 1 - ? new TransactionLoggingView(this.self.paringTransaction).toJSON( + this.self.pairingTransaction && deep === 1 + ? new TransactionLoggingView(this.self.pairingTransaction).toJSON( showBackendTransactions, deep + 1, ) - : { id: this.self.paringTransactionId }, + : { id: this.self.pairingTransaction }, amount: this.decimalToString(this.self.amount), accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation), accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation), diff --git a/dlt-database/entity/0004-fix_spelling/Transaction.ts b/dlt-database/entity/0004-fix_spelling/Transaction.ts new file mode 100644 index 000000000..4d5a304da --- /dev/null +++ b/dlt-database/entity/0004-fix_spelling/Transaction.ts @@ -0,0 +1,128 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToOne, + JoinColumn, + BaseEntity, + OneToMany, +} from 'typeorm' +import { Decimal } from 'decimal.js-light' + +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Account } from '../Account' +import { Community } from '../Community' +import { BackendTransaction } from '../BackendTransaction' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' }) + id: number + + @Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true }) + iotaMessageId?: Buffer + + @OneToOne(() => Transaction, { cascade: ['update'] }) + // eslint-disable-next-line no-use-before-define + pairingTransaction?: Transaction + + @Column({ name: 'pairing_transaction_id', type: 'bigint', unsigned: true, nullable: true }) + pairingTransactionId?: number + + // if transaction has a sender than it is also the sender account + @ManyToOne(() => Account, (account) => account.transactionSigning) + @JoinColumn({ name: 'signing_account_id' }) + signingAccount?: Account + + @Column({ name: 'signing_account_id', type: 'int', unsigned: true, nullable: true }) + signingAccountId?: number + + @ManyToOne(() => Account, (account) => account.transactionRecipient) + @JoinColumn({ name: 'recipient_account_id' }) + recipientAccount?: Account + + @Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true }) + recipientAccountId?: number + + @ManyToOne(() => Community, (community) => community.transactions, { + eager: true, + }) + @JoinColumn({ name: 'community_id' }) + community: Community + + @Column({ name: 'community_id', type: 'int', unsigned: true }) + communityId: number + + @ManyToOne(() => Community, (community) => community.friendCommunitiesTransactions) + @JoinColumn({ name: 'other_community_id' }) + otherCommunity?: Community + + @Column({ name: 'other_community_id', type: 'int', unsigned: true, nullable: true }) + otherCommunityId?: number + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + amount?: Decimal + + // account balance for sender based on creation date + @Column({ + name: 'account_balance_on_creation', + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + accountBalanceOnCreation?: Decimal + + @Column({ type: 'tinyint' }) + type: number + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + @Column({ name: 'body_bytes', type: 'blob' }) + bodyBytes: Buffer + + @Column({ type: 'binary', length: 64, unique: true }) + signature: Buffer + + @Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' }) + protocolVersion: string + + @Column({ type: 'bigint', nullable: true }) + nr?: number + + @Column({ name: 'running_hash', type: 'binary', length: 48, nullable: true }) + runningHash?: Buffer + + // account balance for sender based on confirmation date (iota milestone) + @Column({ + name: 'account_balance_on_confirmation', + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + accountBalanceOnConfirmation?: Decimal + + @Column({ name: 'iota_milestone', type: 'bigint', nullable: true }) + iotaMilestone?: number + + // use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here + @Column({ name: 'confirmed_at', type: 'datetime', nullable: true }) + confirmedAt?: Date + + @OneToMany(() => BackendTransaction, (backendTransaction) => backendTransaction.transaction, { + cascade: ['insert', 'update'], + }) + @JoinColumn({ name: 'transaction_id' }) + backendTransactions: BackendTransaction[] +} diff --git a/dlt-database/entity/Transaction.ts b/dlt-database/entity/Transaction.ts index 113eb3450..9db8e6747 100644 --- a/dlt-database/entity/Transaction.ts +++ b/dlt-database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0003-refactor_transaction_recipe/Transaction' +export { Transaction } from './0004-fix_spelling/Transaction' diff --git a/dlt-database/migrations/0004-fix_spelling.ts b/dlt-database/migrations/0004-fix_spelling.ts new file mode 100644 index 000000000..3b2153a7d --- /dev/null +++ b/dlt-database/migrations/0004-fix_spelling.ts @@ -0,0 +1,15 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + ALTER TABLE \`transactions\` + RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`, + ; + `) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + ALTER TABLE \`transactions\` + RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`, + ; + `) +} From 6500aac5857a06c7cbdab2cef87e997ea5e9df03 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 5 Feb 2024 13:48:17 +0100 Subject: [PATCH 20/51] precompress admin and frontend files, server directly via nginx --- admin/package.json | 1 + .../sites-available/gradido.conf.ssl.template | 27 +++++-------------- .../sites-available/gradido.conf.template | 22 +++------------ deployment/bare_metal/start.sh | 4 +-- frontend/package.json | 1 + 5 files changed, 13 insertions(+), 42 deletions(-) diff --git a/admin/package.json b/admin/package.json index e34136e4b..522fbd592 100644 --- a/admin/package.json +++ b/admin/package.json @@ -10,6 +10,7 @@ "start": "node run/server.js", "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", + "postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +", "dev": "yarn run serve", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index d8ed50ba4..047547e91 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -14,8 +14,8 @@ server { server { server_name $COMMUNITY_HOST; - listen [::]:443 ssl ipv6only=on; - listen 443 ssl; + listen [::]:443 ssl ipv6only=on http2; + listen 443 ssl http2; ssl_certificate $NGINX_SSL_CERTIFICATE; ssl_certificate_key $NGINX_SSL_CERTIFICATE_KEY; include $NGINX_SSL_INCLUDE; @@ -33,7 +33,7 @@ server { return 444; } - #gzip_static on; + gzip_static on; gzip on; gzip_proxied any; gzip_types @@ -53,18 +53,11 @@ server { # Frontend (default) location / { + limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; + root $PROJECT_ROOT/frontend/ - proxy_pass http://127.0.0.1:3000; - proxy_redirect off; - access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; } @@ -119,15 +112,7 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - 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:8080/; - proxy_redirect off; + root $PROJECT_ROOT/admin/ access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index e0f382467..a61fbee47 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -18,7 +18,7 @@ server { return 444; } - #gzip_static on; + gzip_static on; gzip on; gzip_proxied any; gzip_types @@ -40,15 +40,7 @@ server { location / { limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - 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; + root $PROJECT_ROOT/frontend/ access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; @@ -104,15 +96,7 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - 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:8080/; - proxy_redirect off; + root $PROJECT_ROOT/admin/ access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 4b6498ee0..222e13f81 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -237,8 +237,8 @@ export NODE_ENV=production # 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-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-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 save 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' diff --git a/frontend/package.json b/frontend/package.json index fb5210a05..1a18388e2 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,7 @@ "start": "node run/server.js", "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", + "postbuild": "find build -type f -regex '.*\\.\\(html\\|js\\|css\\|svg\\|json\\)' -exec gzip -9 -k {} +", "dev": "yarn run serve", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", From 00d84d5c24bbd7ccd59147b0758bb2025e57faae Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 5 Feb 2024 14:19:44 +0100 Subject: [PATCH 21/51] fix nginx config --- .../nginx/sites-available/gradido.conf.ssl.template | 8 ++++++-- .../nginx/sites-available/gradido.conf.template | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index 047547e91..2079960c5 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -56,7 +56,9 @@ server { limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - root $PROJECT_ROOT/frontend/ + root $PROJECT_ROOT/frontend/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; @@ -112,7 +114,9 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - root $PROJECT_ROOT/admin/ + root $PROJECT_ROOT/admin/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index a61fbee47..d1f5cfece 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -40,7 +40,9 @@ server { location / { limit_req zone=frontend burst=40 nodelay; limit_conn addr 40; - root $PROJECT_ROOT/frontend/ + root $PROJECT_ROOT/frontend/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn; @@ -96,7 +98,9 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; - root $PROJECT_ROOT/admin/ + root $PROJECT_ROOT/admin/build/; + index index.html; + try_files $uri $uri/ /index.html = 404; access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; From 7a6f1655bc9aef2221cb22170c90ad663c58384b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 5 Feb 2024 18:41:48 +0100 Subject: [PATCH 22/51] fix bug with admin --- .../bare_metal/nginx/sites-available/gradido.conf.ssl.template | 1 + .../bare_metal/nginx/sites-available/gradido.conf.template | 1 + 2 files changed, 2 insertions(+) diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index 2079960c5..9910c3366 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -114,6 +114,7 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; + rewrite ^/admin/(.*)$ /$1 break; root $PROJECT_ROOT/admin/build/; index index.html; try_files $uri $uri/ /index.html = 404; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index d1f5cfece..e64e7e1ce 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -98,6 +98,7 @@ server { location /admin { limit_req zone=frontend burst=30 nodelay; limit_conn addr 40; + rewrite ^/admin/(.*)$ /$1 break; root $PROJECT_ROOT/admin/build/; index index.html; try_files $uri $uri/ /index.html = 404; From a60aa7eb4ed52f57629d4d205286a8c6530ccabc Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 13 Feb 2024 12:16:41 +0100 Subject: [PATCH 23/51] typo --- dlt-connector/src/data/Transaction.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlt-connector/src/data/Transaction.logic.ts b/dlt-connector/src/data/Transaction.logic.ts index ec13bf753..c62f78f50 100644 --- a/dlt-connector/src/data/Transaction.logic.ts +++ b/dlt-connector/src/data/Transaction.logic.ts @@ -60,7 +60,7 @@ export class TransactionLogic { return nextTransaction } } - throw new LogError("couldn't find valid paring transaction", { + throw new LogError("couldn't find valid pairing transaction", { id: this.self.id, type: CrossGroupType[type], transactionCountWithSameCreatedAt: sameCreationDateTransactions.length, From c7d6049bd965f0e18fe6567a4ba8397d646f81dc Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 13 Feb 2024 17:28:23 +0100 Subject: [PATCH 24/51] fix test --- .../graphql/resolver/CommunityResolver.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 58dc6a90c..bb58b3e5b 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -164,7 +164,7 @@ describe('CommunityResolver', () => { id: 3, foreign: homeCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), - url: expect.stringMatching('http://localhost/api/2_0'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -175,7 +175,7 @@ describe('CommunityResolver', () => { id: 2, foreign: homeCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), - url: expect.stringMatching('http://localhost/api/1_1'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -186,7 +186,7 @@ describe('CommunityResolver', () => { id: 1, foreign: homeCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), - url: expect.stringMatching('http://localhost/api/1_0'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -236,7 +236,7 @@ describe('CommunityResolver', () => { id: 3, foreign: homeCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), - url: expect.stringMatching('http://localhost/api/2_0'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -247,7 +247,7 @@ describe('CommunityResolver', () => { id: 2, foreign: homeCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), - url: expect.stringMatching('http://localhost/api/1_1'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -258,7 +258,7 @@ describe('CommunityResolver', () => { id: 1, foreign: homeCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), - url: expect.stringMatching('http://localhost/api/1_0'), + endPoint: expect.stringMatching('http://localhost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -269,7 +269,7 @@ describe('CommunityResolver', () => { id: 6, foreign: foreignCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public), - url: expect.stringMatching('http://remotehost/api/1_2'), + endPoint: expect.stringMatching('http://remotehost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -280,7 +280,7 @@ describe('CommunityResolver', () => { id: 5, foreign: foreignCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public), - url: expect.stringMatching('http://remotehost/api/1_1'), + endPoint: expect.stringMatching('http://remotehost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -291,7 +291,7 @@ describe('CommunityResolver', () => { id: 4, foreign: foreignCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public), - url: expect.stringMatching('http://remotehost/api/1_0'), + endPoint: expect.stringMatching('http://remotehost/api/'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, From ff3233e5ff10226287da61addc2450716b9430a0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 14 Feb 2024 09:04:03 +0100 Subject: [PATCH 25/51] Delete .vscode/launch.json --- .vscode/launch.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5a60c21e2..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node-terminal", - "name": "Admin: test", - "request": "launch", - "command": "yarn run test", - "cwd": "${workspaceFolder}/admin" - } - ] -} \ No newline at end of file From 6c16307949aa52608efc1d0af1dc85bed03a8f72 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 15 Feb 2024 20:12:52 +0100 Subject: [PATCH 26/51] fix problems with multiple entries with null id, vue seems automatic use id as key, despite the manuel choosen key publicKey --- admin/src/components/Federation/CommunityVisualizeItem.vue | 2 +- admin/src/graphql/allCommunities.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue index 4a5d9e67e..8d993f1d7 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.vue +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -44,7 +44,7 @@ diff --git a/admin/src/graphql/allCommunities.js b/admin/src/graphql/allCommunities.js index 62010ec90..e14e533df 100644 --- a/admin/src/graphql/allCommunities.js +++ b/admin/src/graphql/allCommunities.js @@ -3,7 +3,6 @@ import gql from 'graphql-tag' export const allCommunities = gql` query { allCommunities { - id foreign url publicKey From b587a51ee855986174450c22f7f8c74cda598161 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 15 Feb 2024 23:29:22 +0100 Subject: [PATCH 27/51] implement suggested changes --- .../components/Federation/CommunityVisualizeItem.vue | 6 +++--- admin/src/graphql/allCommunities.js | 2 +- backend/src/graphql/model/Community.ts | 4 ---- .../src/graphql/resolver/CommunityResolver.test.ts | 11 ++++++++++- backend/src/seeds/graphql/queries.ts | 1 + .../FederatedCommunity.ts | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue index 8d993f1d7..24e6b3712 100644 --- a/admin/src/components/Federation/CommunityVisualizeItem.vue +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -15,8 +15,8 @@ - - {{ $t('federation.communityUuid') }} {{ item.communityUuid }} + + {{ $t('federation.communityUuid') }} {{ item.uuid }} {{ $t('federation.authenticatedAt') }} {{ item.authenticatedAt }} @@ -144,7 +144,7 @@ export default { .mutate({ mutation: updateHomeCommunity, variables: { - uuid: this.item.communityUuid, + uuid: this.item.uuid, gmsApiKey: gmsApiKey, }, }) diff --git a/admin/src/graphql/allCommunities.js b/admin/src/graphql/allCommunities.js index e14e533df..6379086c7 100644 --- a/admin/src/graphql/allCommunities.js +++ b/admin/src/graphql/allCommunities.js @@ -6,7 +6,7 @@ export const allCommunities = gql` foreign url publicKey - communityUuid + uuid authenticatedAt name description diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index 0c4424fa3..db860216a 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -31,7 +31,6 @@ export class Community { if (dbCom.publicKey && dbCom.publicKey.length === 32) { this.publicKey = dbCom.publicKey.toString('hex') } - this.communityUuid = dbCom.communityUuid this.creationDate = dbCom.creationDate this.createdAt = dbCom.createdAt this.updatedAt = dbCom.updatedAt @@ -61,9 +60,6 @@ export class Community { @Field(() => String) publicKey: string - @Field(() => String, { nullable: true }) - communityUuid: string | null - @Field(() => Date, { nullable: true }) creationDate: Date | null diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index bb58b3e5b..af714e746 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -165,6 +165,7 @@ describe('CommunityResolver', () => { foreign: homeCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '2_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -176,6 +177,7 @@ describe('CommunityResolver', () => { foreign: homeCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '1_1', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -187,6 +189,7 @@ describe('CommunityResolver', () => { foreign: homeCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '1_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -222,7 +225,7 @@ describe('CommunityResolver', () => { foreignCom3 = DbFederatedCommunity.create() foreignCom3.foreign = true foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex') - foreignCom3.apiVersion = '1_2' + foreignCom3.apiVersion = '2_0' foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.createdAt = new Date() await DbFederatedCommunity.insert(foreignCom3) @@ -237,6 +240,7 @@ describe('CommunityResolver', () => { foreign: homeCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '2_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -248,6 +252,7 @@ describe('CommunityResolver', () => { foreign: homeCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '1_1', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -259,6 +264,7 @@ describe('CommunityResolver', () => { foreign: homeCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), endPoint: expect.stringMatching('http://localhost/api/'), + apiVersion: '1_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -270,6 +276,7 @@ describe('CommunityResolver', () => { foreign: foreignCom3.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public), endPoint: expect.stringMatching('http://remotehost/api/'), + apiVersion: '2_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -281,6 +288,7 @@ describe('CommunityResolver', () => { foreign: foreignCom2.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public), endPoint: expect.stringMatching('http://remotehost/api/'), + apiVersion: '1_1', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -292,6 +300,7 @@ describe('CommunityResolver', () => { foreign: foreignCom1.foreign, publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public), endPoint: expect.stringMatching('http://remotehost/api/'), + apiVersion: '1_0', lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index f988783b8..c3005ff96 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -157,6 +157,7 @@ export const getCommunities = gql` foreign publicKey endPoint + apiVersion lastAnnouncedAt verifiedAt lastErrorAt diff --git a/database/entity/0083-join_community_federated_communities/FederatedCommunity.ts b/database/entity/0083-join_community_federated_communities/FederatedCommunity.ts index 7e13744c8..b5eb64ea1 100644 --- a/database/entity/0083-join_community_federated_communities/FederatedCommunity.ts +++ b/database/entity/0083-join_community_federated_communities/FederatedCommunity.ts @@ -52,7 +52,7 @@ export class FederatedCommunity extends BaseEntity { }) updatedAt: Date | null - @ManyToOne(() => Community, (community) => community.federatedCommunities) // Assuming you have a User entity with 'accounts' relation + @ManyToOne(() => Community, (community) => community.federatedCommunities) @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) community?: Community } From 4e955d8d9512415dcc033229c315b27268efa2c9 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 16 Feb 2024 00:42:49 +0100 Subject: [PATCH 28/51] refactor --- backend/jest.config.js | 1 + backend/src/graphql/arg/CommunityArgs.ts | 15 +- .../src/graphql/arg/TransactionSendArgs.ts | 2 +- .../src/graphql/input/EditCommunityInput.ts | 14 ++ .../resolver/CommunityResolver.test.ts | 11 +- .../src/graphql/resolver/CommunityResolver.ts | 52 +++--- .../graphql/resolver/TransactionResolver.ts | 4 +- .../src/graphql/resolver/util/communities.ts | 149 ++++++++++++------ .../resolver/util/findUserByIdentifier.ts | 13 +- backend/src/seeds/graphql/queries.ts | 4 +- backend/src/util/validate.ts | 11 +- backend/tsconfig.json | 1 + dlt-connector/.env.dist | 5 +- dlt-connector/.env.template | 5 +- dlt-connector/jest.config.js | 2 +- dlt-connector/package.json | 1 + dlt-connector/src/client/BackendClient.ts | 88 +++++++++++ dlt-connector/src/config/index.ts | 7 +- dlt-connector/src/index.ts | 48 ++++++ dlt-connector/tsconfig.json | 2 +- dlt-connector/yarn.lock | 19 ++- 21 files changed, 343 insertions(+), 111 deletions(-) create mode 100644 backend/src/graphql/input/EditCommunityInput.ts create mode 100644 dlt-connector/src/client/BackendClient.ts diff --git a/backend/jest.config.js b/backend/jest.config.js index f7edec3dd..de649d66e 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -16,6 +16,7 @@ module.exports = { moduleNameMapper: { '@/(.*)': '/src/$1', '@arg/(.*)': '/src/graphql/arg/$1', + '@input/(.*)': '/src/graphql/input/$1', '@dltConnector/(.*)': '/src/apis/dltConnector/$1', '@enum/(.*)': '/src/graphql/enum/$1', '@model/(.*)': '/src/graphql/model/$1', diff --git a/backend/src/graphql/arg/CommunityArgs.ts b/backend/src/graphql/arg/CommunityArgs.ts index 163a6e504..074901e06 100644 --- a/backend/src/graphql/arg/CommunityArgs.ts +++ b/backend/src/graphql/arg/CommunityArgs.ts @@ -1,14 +1,13 @@ -import { IsString } from 'class-validator' -import { Field, ArgsType, InputType } from 'type-graphql' +import { IsBoolean, IsString } from 'class-validator' +import { ArgsType, Field } from 'type-graphql' -@InputType() @ArgsType() export class CommunityArgs { - @Field(() => String) + @Field(() => String, { nullable: true }) @IsString() - uuid: string + communityIdentifier?: string | null - @Field(() => String) - @IsString() - gmsApiKey: string + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + foreign?: boolean | null } diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 5bd8b89f7..3cdb0999e 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { MaxLength, MinLength, IsString } from 'class-validator' +import { MaxLength, MinLength, IsString, IsUUID } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' diff --git a/backend/src/graphql/input/EditCommunityInput.ts b/backend/src/graphql/input/EditCommunityInput.ts new file mode 100644 index 000000000..8c74f874b --- /dev/null +++ b/backend/src/graphql/input/EditCommunityInput.ts @@ -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 +} diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index e720eb716..368d88a2d 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -10,6 +10,7 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql/error/GraphQLError' +import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' @@ -19,7 +20,7 @@ import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' -import { getCommunityByUuid } from './util/communities' +import { getCommunity } from './util/communities' // to do: We need a setup for the tests that closes the connection let mutate: ApolloServerTestClient['mutate'], @@ -459,7 +460,7 @@ describe('CommunityResolver', () => { await mutate({ mutation: login, variables: peterLoginData }) // HomeCommunity is still created in userFactory - homeCom = await getCommunityByUuid(admin.communityUuid) + homeCom = await getCommunity(admin.communityUuid) foreignCom1 = DbCommunity.create() foreignCom1.foreign = true @@ -478,7 +479,7 @@ describe('CommunityResolver', () => { foreignCom2.url = 'http://stage-3.gradido.net/api' foreignCom2.publicKey = Buffer.from('publicKey-stage-3_Community') foreignCom2.privateKey = Buffer.from('privateKey-stage-3_Community') - foreignCom2.communityUuid = 'Stage3-Com-UUID' + foreignCom2.communityUuid = uuidv4() foreignCom2.authenticatedAt = new Date() foreignCom2.name = 'Stage-3_Community-name' foreignCom2.description = 'Stage-3_Community-description' @@ -490,7 +491,7 @@ describe('CommunityResolver', () => { await expect( query({ query: getCommunityByUuidQuery, - variables: { communityUuid: homeCom?.communityUuid }, + variables: { communityIdentifier: homeCom?.communityUuid }, }), ).resolves.toMatchObject({ data: { @@ -563,7 +564,7 @@ describe('CommunityResolver', () => { expect( await mutate({ mutation: updateHomeCommunityQuery, - variables: { uuid: 'unknownUuid', gmsApiKey: 'gmsApiKey' }, + variables: { uuid: uuidv4(), gmsApiKey: 'gmsApiKey' }, }), ).toEqual( expect.objectContaining({ diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 760b982cc..6fac870b1 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,16 +1,17 @@ import { IsNull, Not } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql' +import { Resolver, Query, Authorized, Mutation, Args } from 'type-graphql' import { CommunityArgs } from '@arg//CommunityArgs' +import { EditCommunityInput } from '@input/EditCommunityInput' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { getCommunityByUuid } from './util/communities' +import { getCommunity } from './util/communities' @Resolver() export class CommunityResolver { @@ -41,41 +42,30 @@ export class CommunityResolver { return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } - @Authorized([RIGHTS.COMMUNITY_BY_UUID]) + @Authorized([RIGHTS.COMMUNITIES]) @Query(() => Community) - async community(@Arg('communityUuid') communityUuid: string): Promise { - const com: DbCommunity | null = await getCommunityByUuid(communityUuid) - if (!com) { - throw new LogError('community not found', communityUuid) + async community(@Args() { communityIdentifier, foreign }: CommunityArgs): Promise { + const community = await getCommunity(communityIdentifier, foreign) + if (!community) { + throw new LogError('community not found', communityIdentifier, foreign) } - return new Community(com) + return new Community(community) } @Authorized([RIGHTS.COMMUNITY_UPDATE]) @Mutation(() => Community) - async updateHomeCommunity(@Args() { uuid, gmsApiKey }: CommunityArgs): Promise { - let homeCom: DbCommunity | null - let com: Community - if (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!`) + async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise { + const homeCom = await getCommunity(uuid) + if (!homeCom) { + throw new LogError('HomeCommunity with uuid not found: ', uuid) } - 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) } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ce1ba43da..00894ecd3 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { getCommunityByUuid, getCommunityName, isHomeCommunity } from './util/communities' +import { getCommunity, getCommunityName, isHomeCommunity } from './util/communities' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -452,7 +452,7 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - const recipCom = await getCommunityByUuid(recipientCommunityIdentifier) + const recipCom = await getCommunity(recipientCommunityIdentifier) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index e506548c5..fffe5d6b9 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -1,65 +1,116 @@ +import { FindOptionsWhere } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' +import { isURL } from 'class-validator' +import { LogError } from '@/server/LogError' +import { isUUID4 } from '@/util/validate' + +function getCommunityFindOptions( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): FindOptionsWhere { + if (communityIdentifier === undefined && foreign === undefined) { + throw new LogError('one of communityIdentifier or foreign must be set') + } + + const where: FindOptionsWhere = {} + // != null cover !== null and !== undefined + if (communityIdentifier != null) { + if (typeof communityIdentifier === 'boolean') { + if (typeof foreign === 'boolean') { + throw new LogError('communityIdentifier cannot be boolean if foreign is set separately ') + } + where.foreign = communityIdentifier + } else if (isURL(communityIdentifier)) { + where.url = communityIdentifier + } else if (isUUID4(communityIdentifier)) { + where.communityUuid = communityIdentifier + } + } + + if (typeof foreign === 'boolean') { + where.foreign = foreign + } + return where +} + +/** + * Retrieves a community from the database based on the provided identifier and foreign status. + * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. + * If communityIdentifier is a boolean, it represents the foreign status of the community. + * If foreign is provided separately and is a boolean, it represents the foreign status of the community. + * communityIdentifier and foreign cannot both be boolean + * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. + * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @returns A promise that resolves to a DbCommunity object if found, or null if not found. + */ +export async function getCommunity( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): Promise { + return DbCommunity.findOne({ where: getCommunityFindOptions(communityIdentifier, foreign) }) +} + +/** + * Retrieves a community from the database based on the provided identifier and foreign status. + * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. + * If communityIdentifier is a boolean, it represents the foreign status of the community. + * If foreign is provided separately and is a boolean, it represents the foreign status of the community. + * communityIdentifier and foreign cannot both be boolean + * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. + * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @returns A promise that resolves to a DbCommunity object if found, or throw if not found. + */ +export async function getCommunityOrFail( + communityIdentifier?: string | boolean | null, + foreign?: boolean | null, +): Promise { + return DbCommunity.findOneOrFail({ + where: getCommunityFindOptions(communityIdentifier, foreign), + }) +} + +/** + * 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 { - const homeCommunity = await DbCommunity.findOne({ - where: [ - { foreign: false, communityUuid: communityIdentifier }, - { foreign: false, name: communityIdentifier }, - { foreign: false, url: communityIdentifier }, - ], - }) - if (homeCommunity) { - return true - } else { - return false - } + return (await getCommunity(communityIdentifier, false)) !== null } +/** + * 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 { - return await DbCommunity.findOneOrFail({ - where: [{ foreign: false }], - }) + return getCommunityOrFail(false) } +/** + * 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 { - const community = await DbCommunity.findOneOrFail({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - return community.url + return (await getCommunityOrFail(communityIdentifier)).url } +/** + * 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 { - const community = await DbCommunity.findOne({ - where: [ - { communityUuid: communityIdentifier }, - { name: communityIdentifier }, - { url: communityIdentifier }, - ], - }) - if (community?.authenticatedAt) { - return true - } else { - return false - } + // The !! operator is used to convert the result to a boolean value. + return !!(await getCommunity(communityIdentifier))?.authenticatedAt } +/** + * Retrieves the name 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 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 { - const community = await DbCommunity.findOne({ - where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }], - }) - if (community?.name) { - return community.name - } else { - return '' - } -} - -export async function getCommunityByUuid(communityUuid: string): Promise { - return await DbCommunity.findOne({ - where: [{ communityUuid }], - }) + return (await getCommunity(communityIdentifier))?.name ?? '' } diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 7e52327d3..435dc3d04 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -2,9 +2,11 @@ import { FindOptionsWhere } from '@dbTools/typeorm' import { Community } from '@entity/Community' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' +import { isURL } from 'class-validator' import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' +import { isEMail, isUUID4 } from '@/util/validate' import { VALID_ALIAS_REGEX } from './validateAlias' @@ -19,10 +21,11 @@ export const findUserByIdentifier = async ( communityIdentifier: string, ): Promise => { let user: DbUser | null - const communityWhere: FindOptionsWhere = - validate(communityIdentifier) && version(communityIdentifier) === 4 - ? { communityUuid: communityIdentifier } - : { name: communityIdentifier } + const communityWhere: FindOptionsWhere = isURL(communityIdentifier) + ? { url: communityIdentifier } + : isUUID4(communityIdentifier) + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } if (validate(identifier) && version(identifier) === 4) { user = await DbUser.findOne({ @@ -32,7 +35,7 @@ export const findUserByIdentifier = async ( if (!user) { 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({ where: { email: identifier, diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 6bd106174..9e908d202 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -135,8 +135,8 @@ export const communitiesQuery = gql` ` export const getCommunityByUuidQuery = gql` - query ($communityUuid: String!) { - community(communityUuid: $communityUuid) { + query ($communityIdentifier: String!) { + community(communityIdentifier: $communityIdentifier) { id foreign name diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 4780c94e8..ab0c8a12a 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,5 +1,6 @@ import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { Decimal } from 'decimal.js-light' +import { validate, version } from 'uuid' import { Decay } from '@model/Decay' @@ -16,6 +17,14 @@ function isStringBoolean(value: string): boolean { 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( userId: number, amount: Decimal, @@ -42,4 +51,4 @@ async function calculateBalance( return { balance, lastTransactionId: lastTransaction.id, decay } } -export { calculateBalance, isStringBoolean } +export { calculateBalance, isStringBoolean, isUUID4, isEMail } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 28ddf1c38..c61539e12 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -49,6 +49,7 @@ "paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ "@/*": ["src/*"], "@arg/*": ["src/graphql/arg/*"], + "@input/*": ["src/graphql/input/*"], "@dltConnector/*": ["src/apis/dltConnector/*"], "@enum/*": ["src/graphql/enum/*"], "@model/*": ["src/graphql/model/*"], diff --git a/dlt-connector/.env.dist b/dlt-connector/.env.dist index 1247ac3ec..6f2511c7e 100644 --- a/dlt-connector/.env.dist +++ b/dlt-connector/.env.dist @@ -19,4 +19,7 @@ DB_DATABASE_TEST=gradido_dlt_test TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=6010 \ No newline at end of file +DLT_CONNECTOR_PORT=6010 + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file diff --git a/dlt-connector/.env.template b/dlt-connector/.env.template index e3793f642..a7cbcd6ba 100644 --- a/dlt-connector/.env.template +++ b/dlt-connector/.env.template @@ -15,4 +15,7 @@ DB_DATABASE_TEST=$DB_DATABASE_TEST TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log # DLT-Connector -DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT \ No newline at end of file +DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT + +# Route to Backend +BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 69bc64bb2..e8135c10d 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 66, + lines: 63, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 5bc9673de..146a096e1 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -31,6 +31,7 @@ "express": "4.17.1", "express-slow-down": "^2.0.1", "graphql": "^16.7.1", + "graphql-request": "^6.1.0", "graphql-scalars": "^1.22.2", "helmet": "^7.1.0", "log4js": "^6.7.1", diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts new file mode 100644 index 000000000..2228fdaf8 --- /dev/null +++ b/dlt-connector/src/client/BackendClient.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { gql, GraphQLClient } from 'graphql-request' + +import { CONFIG } from '@/config' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' + +const communityByForeign = gql` + query ($foreign: Boolean) { + community(foreign: $foreign) { + uuid + foreign + creationDate + } + } +` +interface Community { + community: { + 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: 'POST', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } catch (e) { + logger.error("couldn't connect to backend: ", e) + return + } + } + return BackendClient.instance + } + + public async homeCommunityUUid(): Promise { + logger.info('check home community on backend') + const { data, errors } = await this.client.rawRequest(communityByForeign, { + foreign: false, + }) + if (errors) { + throw new LogError('error getting home community from backend', errors) + } + const communityDraft = new CommunityDraft() + communityDraft.uuid = data.community.uuid + communityDraft.foreign = data.community.foreign + communityDraft.createdAt = data.community.creationDate + return communityDraft + } +} diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index e6febb482..d39623c04 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -9,7 +9,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v4.2023-09-12', + EXPECTED: 'v5.2024-02-24', CURRENT: '', }, } @@ -38,6 +38,10 @@ const dltConnector = { 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 constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( @@ -56,4 +60,5 @@ export const CONFIG = { ...database, ...iota, ...dltConnector, + ...backendServer, } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index c72978b35..82bdba658 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,13 +1,61 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import 'reflect-metadata' + import { CONFIG } from '@/config' +import { BackendClient } from './client/BackendClient' +import { CommunityRepository } from './data/Community.repository' +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 { LogError } from './server/LogError' + +async function waitForServer( + backend: BackendClient, + retryIntervalMs: number, + maxRetries: number, +): Promise { + 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.homeCommunityUUid() + } 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() { // eslint-disable-next-line no-console console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`) 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, 1000, 8) + + const communityDraft = await backend.homeCommunityUUid() + const addCommunityContext = new AddCommunityContext(communityDraft) + await addCommunityContext.run() + } + app.listen(CONFIG.DLT_CONNECTOR_PORT, () => { // eslint-disable-next-line no-console console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`) diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index e37b2a7a0..9809d3648 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -63,7 +63,7 @@ "@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["@types", "node_modules/@types"], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 6d50426b1..7fbe1646f 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -569,7 +569,7 @@ "@graphql-typed-document-node/core" "^3.1.1" tslib "^2.4.0" -"@graphql-typed-document-node/core@^3.1.1": +"@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== @@ -2119,6 +2119,13 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3329,6 +3336,14 @@ graphql-query-complexity@^0.12.0: dependencies: lodash.get "^4.4.2" +graphql-request@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" + integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== + dependencies: + "@graphql-typed-document-node/core" "^3.2.0" + cross-fetch "^3.1.5" + graphql-scalars@^1.22.2: version "1.22.2" resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.22.2.tgz#6326e6fe2d0ad4228a9fea72a977e2bf26b86362" @@ -4775,7 +4790,7 @@ node-abort-controller@^3.1.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== -node-fetch@^2.6.7: +node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== From cad31c821129ef15a62558a2275623324fad2f6b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 16 Feb 2024 00:51:51 +0100 Subject: [PATCH 29/51] fix lint --- backend/src/graphql/arg/TransactionSendArgs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 3cdb0999e..5bd8b89f7 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { MaxLength, MinLength, IsString, IsUUID } from 'class-validator' +import { MaxLength, MinLength, IsString } from 'class-validator' import { Decimal } from 'decimal.js-light' import { ArgsType, Field } from 'type-graphql' From f0edd686191833a535aae32f3dc9a4f38b1fde38 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 20 Feb 2024 08:59:59 +0100 Subject: [PATCH 30/51] don't longer misuse communityIdentifier as boolean --- .../resolver/CommunityResolver.test.ts | 2 +- .../src/graphql/resolver/CommunityResolver.ts | 8 +-- .../graphql/resolver/TransactionResolver.ts | 2 +- .../src/graphql/resolver/util/communities.ts | 65 +++++++------------ 4 files changed, 31 insertions(+), 46 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 368d88a2d..c9c76e306 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -460,7 +460,7 @@ describe('CommunityResolver', () => { await mutate({ mutation: login, variables: peterLoginData }) // HomeCommunity is still created in userFactory - homeCom = await getCommunity(admin.communityUuid) + homeCom = await getCommunity({ communityIdentifier: admin.communityUuid }) foreignCom1 = DbCommunity.create() foreignCom1.foreign = true diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 6fac870b1..3162a625e 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -44,10 +44,10 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITIES]) @Query(() => Community) - async community(@Args() { communityIdentifier, foreign }: CommunityArgs): Promise { - const community = await getCommunity(communityIdentifier, foreign) + async community(@Args() communityArgs: CommunityArgs): Promise { + const community = await getCommunity(communityArgs) if (!community) { - throw new LogError('community not found', communityIdentifier, foreign) + throw new LogError('community not found', communityArgs) } return new Community(community) } @@ -55,7 +55,7 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITY_UPDATE]) @Mutation(() => Community) async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise { - const homeCom = await getCommunity(uuid) + const homeCom = await getCommunity({ communityIdentifier: uuid }) if (!homeCom) { throw new LogError('HomeCommunity with uuid not found: ', uuid) } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 00894ecd3..6db74f0f5 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -452,7 +452,7 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - const recipCom = await getCommunity(recipientCommunityIdentifier) + const recipCom = await getCommunity({ communityIdentifier: recipientCommunityIdentifier }) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index fffe5d6b9..0d8850181 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -2,29 +2,26 @@ import { FindOptionsWhere } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { isURL } from 'class-validator' +import { CommunityArgs } from '@/graphql/arg/CommunityArgs' import { LogError } from '@/server/LogError' import { isUUID4 } from '@/util/validate' -function getCommunityFindOptions( - communityIdentifier?: string | boolean | null, - foreign?: boolean | null, -): FindOptionsWhere { +function getCommunityFindOptions({ + communityIdentifier, + foreign, +}: CommunityArgs): FindOptionsWhere { if (communityIdentifier === undefined && foreign === undefined) { throw new LogError('one of communityIdentifier or foreign must be set') } const where: FindOptionsWhere = {} - // != null cover !== null and !== undefined if (communityIdentifier != null) { - if (typeof communityIdentifier === 'boolean') { - if (typeof foreign === 'boolean') { - throw new LogError('communityIdentifier cannot be boolean if foreign is set separately ') - } - where.foreign = communityIdentifier - } else if (isURL(communityIdentifier)) { + if (isURL(communityIdentifier)) { where.url = communityIdentifier } else if (isUUID4(communityIdentifier)) { where.communityUuid = communityIdentifier + } else { + where.name = communityIdentifier } } @@ -36,37 +33,23 @@ function getCommunityFindOptions( /** * Retrieves a community from the database based on the provided identifier and foreign status. - * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. - * If communityIdentifier is a boolean, it represents the foreign status of the community. - * If foreign is provided separately and is a boolean, it represents the foreign status of the community. - * communityIdentifier and foreign cannot both be boolean - * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. - * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community + * @param foreign Optional. If provided it represents the foreign status of the community. * @returns A promise that resolves to a DbCommunity object if found, or null if not found. */ -export async function getCommunity( - communityIdentifier?: string | boolean | null, - foreign?: boolean | null, -): Promise { - return DbCommunity.findOne({ where: getCommunityFindOptions(communityIdentifier, foreign) }) +export async function getCommunity(communityArgs: CommunityArgs): Promise { + return DbCommunity.findOne({ where: getCommunityFindOptions(communityArgs) }) } /** * Retrieves a community from the database based on the provided identifier and foreign status. - * If communityIdentifier is a string, it can represent either the URL, UUID, or name of the community. - * If communityIdentifier is a boolean, it represents the foreign status of the community. - * If foreign is provided separately and is a boolean, it represents the foreign status of the community. - * communityIdentifier and foreign cannot both be boolean - * @param communityIdentifier The identifier (URL, UUID, or name) of the community, or a boolean representing the foreign status. - * @param foreign Optional. If provided and is a boolean, it represents the foreign status of the community. + * @param communityIdentifier The identifier (URL, UUID, or name) of the community + * @param foreign Optional. If provided it represents the foreign status of the community. * @returns A promise that resolves to a DbCommunity object if found, or throw if not found. */ -export async function getCommunityOrFail( - communityIdentifier?: string | boolean | null, - foreign?: boolean | null, -): Promise { +export async function getCommunityOrFail(communityArgs: CommunityArgs): Promise { return DbCommunity.findOneOrFail({ - where: getCommunityFindOptions(communityIdentifier, foreign), + where: getCommunityFindOptions(communityArgs), }) } @@ -76,7 +59,7 @@ export async function getCommunityOrFail( * @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 { - return (await getCommunity(communityIdentifier, false)) !== null + return (await getCommunity({ communityIdentifier, foreign: false })) !== null } /** @@ -84,7 +67,7 @@ export async function isHomeCommunity(communityIdentifier: string): Promise { - return getCommunityOrFail(false) + return getCommunityOrFail({ foreign: false }) } /** @@ -93,24 +76,26 @@ export async function getHomeCommunity(): Promise { * @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 { - return (await getCommunityOrFail(communityIdentifier)).url + return (await getCommunityOrFail({ communityIdentifier })).url } /** * 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. + * @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 { // The !! operator is used to convert the result to a boolean value. - return !!(await getCommunity(communityIdentifier))?.authenticatedAt + return !!(await getCommunity({ communityIdentifier }))?.authenticatedAt } /** * Retrieves the name 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 name of the community. If the community does not exist or has no name, an empty string is returned. + * @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 { - return (await getCommunity(communityIdentifier))?.name ?? '' + return (await getCommunity({ communityIdentifier }))?.name ?? '' } From 4255c1fdfa86609dace0f36cb00b10fd22e92bd4 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 20 Feb 2024 12:47:40 +0100 Subject: [PATCH 31/51] dlt-connector use jwt token for authentication at backend --- backend/src/auth/ADMIN_RIGHTS.ts | 1 + backend/src/auth/DLT_CONNECTOR_RIGHTS.ts | 3 + backend/src/auth/RIGHTS.ts | 1 + backend/src/auth/ROLES.ts | 3 + backend/src/graphql/directive/isAuthorized.ts | 58 +++++++++++-------- backend/src/graphql/enum/RoleNames.ts | 1 + dlt-connector/.env.dist | 5 +- dlt-connector/.env.template | 2 + dlt-connector/package.json | 1 + dlt-connector/src/client/BackendClient.ts | 14 +++++ dlt-connector/src/config/index.ts | 3 +- .../community/HomeCommunity.role.ts | 15 ++++- dlt-connector/yarn.lock | 5 ++ 13 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 backend/src/auth/DLT_CONNECTOR_RIGHTS.ts diff --git a/backend/src/auth/ADMIN_RIGHTS.ts b/backend/src/auth/ADMIN_RIGHTS.ts index 79006a1de..db0b2a4d1 100644 --- a/backend/src/auth/ADMIN_RIGHTS.ts +++ b/backend/src/auth/ADMIN_RIGHTS.ts @@ -6,4 +6,5 @@ export const ADMIN_RIGHTS = [ RIGHTS.UNDELETE_USER, RIGHTS.COMMUNITY_UPDATE, RIGHTS.COMMUNITY_BY_UUID, + RIGHTS.COMMUNITY_BY_IDENTIFIER, ] diff --git a/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts new file mode 100644 index 000000000..3186082b8 --- /dev/null +++ b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts @@ -0,0 +1,3 @@ +import { RIGHTS } from './RIGHTS' + +export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index c95aa18fd..670302e1d 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -59,5 +59,6 @@ export enum RIGHTS { DELETE_USER = 'DELETE_USER', UNDELETE_USER = 'UNDELETE_USER', COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID', + COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER', COMMUNITY_UPDATE = 'COMMUNITY_UPDATE', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 15ba7b263..58b127626 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -1,6 +1,7 @@ import { RoleNames } from '@/graphql/enum/RoleNames' import { ADMIN_RIGHTS } from './ADMIN_RIGHTS' +import { DLT_CONNECTOR_RIGHTS } from './DLT_CONNECTOR_RIGHTS' import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS' import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS' import { Role } from './Role' @@ -20,5 +21,7 @@ export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [ ...ADMIN_RIGHTS, ]) +export const ROLE_DLT_CONNECTOR = new Role(RoleNames.DLT_CONNECTOR_ROLE, DLT_CONNECTOR_RIGHTS) + // TODO from database export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN] diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 59309c91e..f3d03a539 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -6,7 +6,13 @@ import { RoleNames } from '@enum/RoleNames' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' import { decode, encode } from '@/auth/JWT' 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 { LogError } from '@/server/LogError' @@ -30,31 +36,35 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => // Set context gradidoID context.gradidoID = decoded.gradidoID - // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests - // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey - try { - const user = await User.findOneOrFail({ - where: { gradidoID: decoded.gradidoID }, - withDeleted: true, - relations: ['emailContact', 'userRoles'], - }) - context.user = user - context.role = ROLE_USER - if (user.userRoles?.length > 0) { - switch (user.userRoles[0].role) { - case RoleNames.ADMIN: - context.role = ROLE_ADMIN - break - case RoleNames.MODERATOR: - context.role = ROLE_MODERATOR - break - default: - context.role = ROLE_USER + if (context.gradidoID === 'dlt-connector') { + context.role = ROLE_DLT_CONNECTOR + } else { + // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests + // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey + try { + const user = await User.findOneOrFail({ + where: { gradidoID: decoded.gradidoID }, + withDeleted: true, + relations: ['emailContact', 'userRoles'], + }) + context.user = user + context.role = ROLE_USER + if (user.userRoles?.length > 0) { + switch (user.userRoles[0].role) { + case RoleNames.ADMIN: + context.role = ROLE_ADMIN + break + case RoleNames.MODERATOR: + 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 diff --git a/backend/src/graphql/enum/RoleNames.ts b/backend/src/graphql/enum/RoleNames.ts index c4a9b25cc..431154524 100644 --- a/backend/src/graphql/enum/RoleNames.ts +++ b/backend/src/graphql/enum/RoleNames.ts @@ -5,6 +5,7 @@ export enum RoleNames { USER = 'USER', MODERATOR = 'MODERATOR', ADMIN = 'ADMIN', + DLT_CONNECTOR_ROLE = 'DLT_CONNECTOR_ROLE', } registerEnumType(RoleNames, { diff --git a/dlt-connector/.env.dist b/dlt-connector/.env.dist index 6f2511c7e..50e9fe8e1 100644 --- a/dlt-connector/.env.dist +++ b/dlt-connector/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v4.2023-09-12 +CONFIG_VERSION=v6.2024-02-20 # SET LOG LEVEL AS NEEDED IN YOUR .ENV # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal @@ -22,4 +22,5 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log DLT_CONNECTOR_PORT=6010 # Route to Backend -BACKEND_SERVER_URL=http://localhost:4000 \ No newline at end of file +BACKEND_SERVER_URL=http://localhost:4000 +JWT_SECRET=secret123 \ No newline at end of file diff --git a/dlt-connector/.env.template b/dlt-connector/.env.template index a7cbcd6ba..2e123ca81 100644 --- a/dlt-connector/.env.template +++ b/dlt-connector/.env.template @@ -1,5 +1,7 @@ CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION +JWT_SECRET=$JWT_SECRET + #IOTA IOTA_API_URL=$IOTA_API_URL IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 146a096e1..b491a7c22 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -34,6 +34,7 @@ "graphql-request": "^6.1.0", "graphql-scalars": "^1.22.2", "helmet": "^7.1.0", + "jose": "^5.2.2", "log4js": "^6.7.1", "nodemon": "^2.0.20", "protobufjs": "^7.2.5", diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts index 2228fdaf8..0a460d116 100644 --- a/dlt-connector/src/client/BackendClient.ts +++ b/dlt-connector/src/client/BackendClient.ts @@ -6,6 +6,7 @@ import { CONFIG } from '@/config' import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' +import { SignJWT } from 'jose' const communityByForeign = gql` query ($foreign: Boolean) { @@ -73,6 +74,7 @@ export class BackendClient { public async homeCommunityUUid(): Promise { logger.info('check home community on backend') + this.client.setHeader('token', await this.createJWTToken()) const { data, errors } = await this.client.rawRequest(communityByForeign, { foreign: false, }) @@ -85,4 +87,16 @@ export class BackendClient { communityDraft.createdAt = data.community.creationDate return communityDraft } + + private async createJWTToken(): Promise { + 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 + } } diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index d39623c04..0a37d6413 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -9,13 +9,14 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v5.2024-02-24', + EXPECTED: 'v6.2024-02-20', CURRENT: '', }, } const server = { PRODUCTION: process.env.NODE_ENV === 'production' ?? false, + JWT_SECRET: process.env.JWT_SECRET ?? 'secret123', } const database = { diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 7a4798368..bdee42ae4 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -10,6 +10,7 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionError } from '@/graphql/model/TransactionError' import { CommunityLoggingView } from '@/logging/CommunityLogging.view' import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' @@ -22,7 +23,19 @@ export class HomeCommunityRole extends CommunityRole { public async create(communityDraft: CommunityDraft, topic: string): Promise { super.create(communityDraft, topic) // 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) // create auf account and gmw account diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 7fbe1646f..81e904cf3 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -4281,6 +4281,11 @@ jiti@^1.19.3: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== +jose@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.2.tgz#b91170e9ba6dbe609b0c0a86568f9a1fbe4335c0" + integrity sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" From e5e22c394d443857b9a9ae922870222e380599a2 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 20 Feb 2024 13:22:32 +0100 Subject: [PATCH 32/51] update test --- .../resolver/CommunityResolver.test.ts | 28 +++++++++++++++++-- .../src/graphql/resolver/CommunityResolver.ts | 2 +- backend/src/seeds/graphql/queries.ts | 6 ++-- dlt-connector/src/client/BackendClient.ts | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index c9c76e306..5c35a8570 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -17,7 +17,7 @@ import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' -import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries' +import { getCommunities, communitiesQuery, getCommunityQuery } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' import { getCommunity } from './util/communities' @@ -487,10 +487,10 @@ describe('CommunityResolver', () => { await DbCommunity.insert(foreignCom2) }) - it('finds the home-community', async () => { + it('finds the home-community by uuid', async () => { await expect( query({ - query: getCommunityByUuidQuery, + query: getCommunityQuery, variables: { communityIdentifier: homeCom?.communityUuid }, }), ).resolves.toMatchObject({ @@ -509,6 +509,28 @@ describe('CommunityResolver', () => { }) }) + it('finds the home-community by foreign', async () => { + await expect( + query({ + query: getCommunityQuery, + variables: { foreign: false }, + }), + ).resolves.toMatchObject({ + data: { + community: { + 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('updates the home-community gmsApiKey', async () => { await expect( mutate({ diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 3162a625e..75ac4e8bc 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -42,7 +42,7 @@ export class CommunityResolver { return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } - @Authorized([RIGHTS.COMMUNITIES]) + @Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER]) @Query(() => Community) async community(@Args() communityArgs: CommunityArgs): Promise { const community = await getCommunity(communityArgs) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 9e908d202..49dec61b5 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -134,9 +134,9 @@ export const communitiesQuery = gql` } ` -export const getCommunityByUuidQuery = gql` - query ($communityIdentifier: String!) { - community(communityIdentifier: $communityIdentifier) { +export const getCommunityQuery = gql` + query ($communityIdentifier: String, $foreign: Boolean) { + community(communityIdentifier: $communityIdentifier, foreign: $foreign) { id foreign name diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts index 0a460d116..80bcaa132 100644 --- a/dlt-connector/src/client/BackendClient.ts +++ b/dlt-connector/src/client/BackendClient.ts @@ -1,12 +1,12 @@ /* 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' -import { SignJWT } from 'jose' const communityByForeign = gql` query ($foreign: Boolean) { From 644a37384737149a2ccf8d75f1f3be229a8647a7 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 20 Feb 2024 13:58:02 +0100 Subject: [PATCH 33/51] better function name --- dlt-connector/src/client/BackendClient.ts | 15 +++++++++------ dlt-connector/src/index.ts | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts index 80bcaa132..68745fd5a 100644 --- a/dlt-connector/src/client/BackendClient.ts +++ b/dlt-connector/src/client/BackendClient.ts @@ -58,7 +58,7 @@ export class BackendClient { headers: { 'content-type': 'application/json', }, - method: 'POST', + method: 'GET', jsonSerializer: { parse: JSON.parse, stringify: JSON.stringify, @@ -72,12 +72,15 @@ export class BackendClient { return BackendClient.instance } - public async homeCommunityUUid(): Promise { + public async getHomeCommunityDraft(): Promise { logger.info('check home community on backend') - this.client.setHeader('token', await this.createJWTToken()) - const { data, errors } = await this.client.rawRequest(communityByForeign, { - foreign: false, - }) + const { data, errors } = await this.client.rawRequest( + communityByForeign, + { + foreign: false, + }, + { authorization: 'Bearer ' + (await this.createJWTToken()) }, + ) if (errors) { throw new LogError('error getting home community from backend', errors) } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 82bdba658..28b8b0a0e 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -22,7 +22,7 @@ async function waitForServer( try { // Make a HEAD request to the server - return await backend.homeCommunityUUid() + return await backend.getHomeCommunityDraft() } catch (error) { logger.info('Server is not reachable: ', error) } @@ -51,7 +51,7 @@ async function main() { // wait for backend server to be ready await waitForServer(backend, 1000, 8) - const communityDraft = await backend.homeCommunityUUid() + const communityDraft = await backend.getHomeCommunityDraft() const addCommunityContext = new AddCommunityContext(communityDraft) await addCommunityContext.run() } From abb8cdcf3d45d37fd64af6112364ebafc62835f4 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 21 Feb 2024 15:34:52 +0100 Subject: [PATCH 34/51] re-refactor, remove 'complex' code --- backend/src/auth/ADMIN_RIGHTS.ts | 1 + backend/src/auth/DLT_CONNECTOR_RIGHTS.ts | 2 +- backend/src/auth/RIGHTS.ts | 1 + .../resolver/CommunityResolver.test.ts | 17 ++- .../src/graphql/resolver/CommunityResolver.ts | 23 ++-- .../graphql/resolver/TransactionResolver.ts | 4 +- .../src/graphql/resolver/util/communities.ts | 101 ++++++++---------- backend/src/seeds/graphql/queries.ts | 22 +++- dlt-connector/src/client/BackendClient.ts | 20 ++-- dlt-connector/src/index.ts | 2 +- 10 files changed, 103 insertions(+), 90 deletions(-) diff --git a/backend/src/auth/ADMIN_RIGHTS.ts b/backend/src/auth/ADMIN_RIGHTS.ts index db0b2a4d1..e95935fd0 100644 --- a/backend/src/auth/ADMIN_RIGHTS.ts +++ b/backend/src/auth/ADMIN_RIGHTS.ts @@ -7,4 +7,5 @@ export const ADMIN_RIGHTS = [ RIGHTS.COMMUNITY_UPDATE, RIGHTS.COMMUNITY_BY_UUID, RIGHTS.COMMUNITY_BY_IDENTIFIER, + RIGHTS.HOME_COMMUNITY, ] diff --git a/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts index 3186082b8..399b7c2d4 100644 --- a/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts +++ b/backend/src/auth/DLT_CONNECTOR_RIGHTS.ts @@ -1,3 +1,3 @@ import { RIGHTS } from './RIGHTS' -export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER] +export const DLT_CONNECTOR_RIGHTS = [RIGHTS.COMMUNITY_BY_IDENTIFIER, RIGHTS.HOME_COMMUNITY] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 670302e1d..c8f02976b 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -60,5 +60,6 @@ export enum RIGHTS { UNDELETE_USER = 'UNDELETE_USER', COMMUNITY_BY_UUID = 'COMMUNITY_BY_UUID', COMMUNITY_BY_IDENTIFIER = 'COMMUNITY_BY_IDENTIFIER', + HOME_COMMUNITY = 'HOME_COMMUNITY', COMMUNITY_UPDATE = 'COMMUNITY_UPDATE', } diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 5c35a8570..6b4da54cf 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -17,10 +17,10 @@ import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' -import { getCommunities, communitiesQuery, getCommunityQuery } from '@/seeds/graphql/queries' +import { getCommunities, communitiesQuery, getHomeCommunityQuery, getCommunityByIdentifierQuery } from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' -import { getCommunity } from './util/communities' +import { getCommunityByUuid } from './util/communities' // to do: We need a setup for the tests that closes the connection let mutate: ApolloServerTestClient['mutate'], @@ -460,7 +460,7 @@ describe('CommunityResolver', () => { await mutate({ mutation: login, variables: peterLoginData }) // HomeCommunity is still created in userFactory - homeCom = await getCommunity({ communityIdentifier: admin.communityUuid }) + homeCom = await getCommunityByUuid(admin.communityUuid) foreignCom1 = DbCommunity.create() foreignCom1.foreign = true @@ -490,12 +490,12 @@ describe('CommunityResolver', () => { it('finds the home-community by uuid', async () => { await expect( query({ - query: getCommunityQuery, + query: getCommunityByIdentifierQuery, variables: { communityIdentifier: homeCom?.communityUuid }, }), ).resolves.toMatchObject({ data: { - community: { + communityByIdentifier: { id: homeCom?.id, foreign: homeCom?.foreign, name: homeCom?.name, @@ -509,15 +509,14 @@ describe('CommunityResolver', () => { }) }) - it('finds the home-community by foreign', async () => { + it('finds the home-community', async () => { await expect( query({ - query: getCommunityQuery, - variables: { foreign: false }, + query: getHomeCommunityQuery, }), ).resolves.toMatchObject({ data: { - community: { + homeCommunity: { id: homeCom?.id, foreign: homeCom?.foreign, name: homeCom?.name, diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 75ac4e8bc..951a8dcb0 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,9 +1,8 @@ import { IsNull, Not } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { Resolver, Query, Authorized, Mutation, Args } from 'type-graphql' +import { Resolver, Query, Authorized, Mutation, Args, Arg } from 'type-graphql' -import { CommunityArgs } from '@arg//CommunityArgs' import { EditCommunityInput } from '@input/EditCommunityInput' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' @@ -11,7 +10,7 @@ import { FederatedCommunity } from '@model/FederatedCommunity' import { RIGHTS } from '@/auth/RIGHTS' import { LogError } from '@/server/LogError' -import { getCommunity } from './util/communities' +import { getCommunityByIdentifier, getCommunityByUuid, getHomeCommunity } from './util/communities' @Resolver() export class CommunityResolver { @@ -44,10 +43,20 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER]) @Query(() => Community) - async community(@Args() communityArgs: CommunityArgs): Promise { - const community = await getCommunity(communityArgs) + async communityByIdentifier(@Arg('communityIdentifier') communityIdentifier: string): Promise { + const community = await getCommunityByIdentifier(communityIdentifier) if (!community) { - throw new LogError('community not found', communityArgs) + throw new LogError('community not found', communityIdentifier) + } + return new Community(community) + } + + @Authorized([RIGHTS.HOME_COMMUNITY]) + @Query(() => Community) + async homeCommunity(): Promise { + const community = await getHomeCommunity() + if (!community) { + throw new LogError('no home community exist') } return new Community(community) } @@ -55,7 +64,7 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITY_UPDATE]) @Mutation(() => Community) async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise { - const homeCom = await getCommunity({ communityIdentifier: uuid }) + const homeCom = await getCommunityByUuid(uuid) if (!homeCom) { throw new LogError('HomeCommunity with uuid not found: ', uuid) } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 6db74f0f5..56c928995 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { getCommunity, getCommunityName, isHomeCommunity } from './util/communities' +import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -452,7 +452,7 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - const recipCom = await getCommunity({ communityIdentifier: recipientCommunityIdentifier }) + const recipCom = await getCommunityByIdentifier(recipientCommunityIdentifier) logger.debug('recipient commuity: ', recipCom) if (recipCom === null) { throw new LogError( diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index 0d8850181..790a4d67c 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -1,56 +1,14 @@ -import { FindOptionsWhere } from '@dbTools/typeorm' +import { FindOneOptions } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' -import { isURL } from 'class-validator' -import { CommunityArgs } from '@/graphql/arg/CommunityArgs' -import { LogError } from '@/server/LogError' -import { isUUID4 } from '@/util/validate' - -function getCommunityFindOptions({ - communityIdentifier, - foreign, -}: CommunityArgs): FindOptionsWhere { - if (communityIdentifier === undefined && foreign === undefined) { - throw new LogError('one of communityIdentifier or foreign must be set') +function findWithCommunityIdentifier(communityIdentifier: string): FindOneOptions { + return { + where: [ + { communityUuid: communityIdentifier }, + { name: communityIdentifier }, + { url: communityIdentifier }, + ], } - - const where: FindOptionsWhere = {} - if (communityIdentifier != null) { - if (isURL(communityIdentifier)) { - where.url = communityIdentifier - } else if (isUUID4(communityIdentifier)) { - where.communityUuid = communityIdentifier - } else { - where.name = communityIdentifier - } - } - - if (typeof foreign === 'boolean') { - where.foreign = foreign - } - return where -} - -/** - * Retrieves a community from the database based on the provided identifier and foreign status. - * @param communityIdentifier The identifier (URL, UUID, or name) of the community - * @param foreign Optional. If provided it represents the foreign status of the community. - * @returns A promise that resolves to a DbCommunity object if found, or null if not found. - */ -export async function getCommunity(communityArgs: CommunityArgs): Promise { - return DbCommunity.findOne({ where: getCommunityFindOptions(communityArgs) }) -} - -/** - * Retrieves a community from the database based on the provided identifier and foreign status. - * @param communityIdentifier The identifier (URL, UUID, or name) of the community - * @param foreign Optional. If provided it represents the foreign status of the community. - * @returns A promise that resolves to a DbCommunity object if found, or throw if not found. - */ -export async function getCommunityOrFail(communityArgs: CommunityArgs): Promise { - return DbCommunity.findOneOrFail({ - where: getCommunityFindOptions(communityArgs), - }) } /** @@ -59,7 +17,15 @@ export async function getCommunityOrFail(communityArgs: CommunityArgs): Promise< * @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 { - return (await getCommunity({ communityIdentifier, foreign: false })) !== null + // 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: [ + { foreign: false, communityUuid: communityIdentifier }, + { foreign: false, name: communityIdentifier }, + { foreign: false, url: communityIdentifier }, + ], + })) } /** @@ -67,35 +33,56 @@ export async function isHomeCommunity(communityIdentifier: string): Promise { - return getCommunityOrFail({ foreign: false }) + return await DbCommunity.findOneOrFail({ + 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 { - return (await getCommunityOrFail({ communityIdentifier })).url + return (await DbCommunity.findOneOrFail(findWithCommunityIdentifier(communityIdentifier))).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 { - // The !! operator is used to convert the result to a boolean value. - return !!(await getCommunity({ communityIdentifier }))?.authenticatedAt + // 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(findWithCommunityIdentifier(communityIdentifier))) + ?.authenticatedAt } /** * Retrieves the name of the community with the given identifier. - * @param communityIdentifier The identifier (URL, UUID, or name) of the community. + * @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 { - return (await getCommunity({ communityIdentifier }))?.name ?? '' + const community = await DbCommunity.findOne({ + where: [{ communityUuid: communityIdentifier }, { url: communityIdentifier }], + }) + + return community?.name ? community.name : '' +} +export async function getCommunityByUuid(communityUuid: string): Promise { + return await DbCommunity.findOne({ + where: [{ communityUuid }], + }) +} + +export async function getCommunityByIdentifier( + communityIdentifier: string, +): Promise { + return await DbCommunity.findOne(findWithCommunityIdentifier(communityIdentifier)) } diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 49dec61b5..0afbc2db2 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -134,9 +134,25 @@ export const communitiesQuery = gql` } ` -export const getCommunityQuery = gql` - query ($communityIdentifier: String, $foreign: Boolean) { - community(communityIdentifier: $communityIdentifier, foreign: $foreign) { +export const getCommunityByIdentifierQuery = gql` + query ($communityIdentifier: String!) { + communityByIdentifier(communityIdentifier: $communityIdentifier) { + id + foreign + name + description + url + creationDate + uuid + authenticatedAt + gmsApiKey + } + } +` + +export const getHomeCommunityQuery = gql` + query { + homeCommunity { id foreign name diff --git a/dlt-connector/src/client/BackendClient.ts b/dlt-connector/src/client/BackendClient.ts index 68745fd5a..77356f5d8 100644 --- a/dlt-connector/src/client/BackendClient.ts +++ b/dlt-connector/src/client/BackendClient.ts @@ -8,9 +8,9 @@ import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' -const communityByForeign = gql` - query ($foreign: Boolean) { - community(foreign: $foreign) { +const homeCommunity = gql` + query { + homeCommunity { uuid foreign creationDate @@ -18,7 +18,7 @@ const communityByForeign = gql` } ` interface Community { - community: { + homeCommunity: { uuid: string foreign: boolean creationDate: string @@ -75,19 +75,19 @@ export class BackendClient { public async getHomeCommunityDraft(): Promise { logger.info('check home community on backend') const { data, errors } = await this.client.rawRequest( - communityByForeign, + homeCommunity, + {}, { - foreign: false, + authorization: 'Bearer ' + (await this.createJWTToken()), }, - { authorization: 'Bearer ' + (await this.createJWTToken()) }, ) if (errors) { throw new LogError('error getting home community from backend', errors) } const communityDraft = new CommunityDraft() - communityDraft.uuid = data.community.uuid - communityDraft.foreign = data.community.foreign - communityDraft.createdAt = data.community.creationDate + communityDraft.uuid = data.homeCommunity.uuid + communityDraft.foreign = data.homeCommunity.foreign + communityDraft.createdAt = data.homeCommunity.creationDate return communityDraft } diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 28b8b0a0e..bec35e1df 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -49,7 +49,7 @@ async function main() { throw new LogError('cannot create backend client') } // wait for backend server to be ready - await waitForServer(backend, 1000, 8) + await waitForServer(backend, 2500, 10) const communityDraft = await backend.getHomeCommunityDraft() const addCommunityContext = new AddCommunityContext(communityDraft) From 6ba5d4b63511009f3c70aa2d98df665f46994f5f Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 21 Feb 2024 15:41:12 +0100 Subject: [PATCH 35/51] fix linting --- backend/src/graphql/resolver/CommunityResolver.test.ts | 7 ++++++- backend/src/graphql/resolver/CommunityResolver.ts | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 6b4da54cf..25c5bfcf7 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -17,7 +17,12 @@ import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' -import { getCommunities, communitiesQuery, getHomeCommunityQuery, getCommunityByIdentifierQuery } from '@/seeds/graphql/queries' +import { + getCommunities, + communitiesQuery, + getHomeCommunityQuery, + getCommunityByIdentifierQuery, +} from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' import { getCommunityByUuid } from './util/communities' diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 951a8dcb0..eb320586e 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -43,7 +43,9 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITY_BY_IDENTIFIER]) @Query(() => Community) - async communityByIdentifier(@Arg('communityIdentifier') communityIdentifier: string): Promise { + async communityByIdentifier( + @Arg('communityIdentifier') communityIdentifier: string, + ): Promise { const community = await getCommunityByIdentifier(communityIdentifier) if (!community) { throw new LogError('community not found', communityIdentifier) From d187c541da42311e14cd2ec267eb908818fb2258 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 21 Feb 2024 18:15:54 +0100 Subject: [PATCH 36/51] move adjustet Community Model Code into separat Model Class --- .../src/graphql/model/AdminCommunityView.ts | 76 +++++++++++++++++++ backend/src/graphql/model/Community.ts | 46 ++--------- .../src/graphql/resolver/CommunityResolver.ts | 7 +- 3 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 backend/src/graphql/model/AdminCommunityView.ts diff --git a/backend/src/graphql/model/AdminCommunityView.ts b/backend/src/graphql/model/AdminCommunityView.ts new file mode 100644 index 000000000..95af54bc5 --- /dev/null +++ b/backend/src/graphql/model/AdminCommunityView.ts @@ -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 +} diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index db860216a..06c07875e 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -1,39 +1,15 @@ import { Community as DbCommunity } from '@entity/Community' -import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ObjectType, Field, Int } from 'type-graphql' -import { FederatedCommunity } from './FederatedCommunity' - @ObjectType() export class Community { 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), - ) - } - this.id = dbCom.id ?? 0 - if (dbCom.foreign !== undefined) { - this.foreign = dbCom.foreign - } + this.id = dbCom.id + 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.url = dbCom.url this.creationDate = dbCom.creationDate - this.createdAt = dbCom.createdAt - this.updatedAt = dbCom.updatedAt this.uuid = dbCom.communityUuid this.authenticatedAt = dbCom.authenticatedAt this.gmsApiKey = dbCom.gmsApiKey @@ -51,30 +27,18 @@ export class Community { @Field(() => String, { nullable: true }) description: string | null - @Field(() => String, { nullable: true }) - gmsApiKey: string | null - @Field(() => String) url: string - @Field(() => String) - publicKey: string - @Field(() => Date, { nullable: true }) creationDate: Date | null - @Field(() => Date, { nullable: true }) - createdAt: Date | null - - @Field(() => Date, { nullable: true }) - updatedAt: Date | null - @Field(() => String, { nullable: true }) uuid: string | null @Field(() => Date, { nullable: true }) authenticatedAt: Date | null - @Field(() => [FederatedCommunity], { nullable: true }) - federatedCommunities: FederatedCommunity[] | null + @Field(() => String, { nullable: true }) + gmsApiKey: string | null } diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index d6e69694e..9cab50610 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -5,6 +5,7 @@ import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql' import { CommunityArgs } from '@arg//CommunityArgs' import { Paginated } from '@arg/Paginated' +import { AdminCommunityView } from '@model/AdminCommunityView' import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' @@ -31,9 +32,9 @@ export class CommunityResolver { } @Authorized([RIGHTS.COMMUNITIES]) - @Query(() => [Community]) - async allCommunities(@Args() paginated: Paginated): Promise { - return (await getAllCommunities(paginated)).map((dbCom) => new Community(dbCom)) + @Query(() => [AdminCommunityView]) + async allCommunities(@Args() paginated: Paginated): Promise { + return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom)) } @Authorized([RIGHTS.COMMUNITIES]) From 7aa75b0abd3600b5fce01914b4da01b985b20eb3 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Wed, 21 Feb 2024 19:29:12 +0100 Subject: [PATCH 37/51] add test --- .../resolver/CommunityResolver.test.ts | 230 +++++++++++++++++- backend/src/seeds/graphql/queries.ts | 28 +++ 2 files changed, 257 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index af714e746..0e4bd5e70 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -16,7 +16,12 @@ import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations' -import { getCommunities, communitiesQuery, getCommunityByUuidQuery } from '@/seeds/graphql/queries' +import { + getCommunities, + communitiesQuery, + getCommunityByUuidQuery, + allCommunities, +} from '@/seeds/graphql/queries' import { peterLustig } from '@/seeds/users/peter-lustig' import { getCommunityByUuid } from './util/communities' @@ -312,6 +317,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', () => { diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index c3005ff96..c3e7974a4 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -167,6 +167,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` query ($code: String!) { queryTransactionLink(code: $code) { From 84b39089c2dd4d79d86e11b555963df764989b90 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 22 Feb 2024 08:46:41 +0100 Subject: [PATCH 38/51] remove .nvmrc entry from gitignore, add nvmrc with same version like docker per module --- .gitignore | 3 --- admin/.nvmrc | 1 + backend/.nvmrc | 1 + database/.nvmrc | 1 + dht-node/.nvmrc | 1 + dlt-connector/.nvmrc | 1 + dlt-database/.nvmrc | 1 + federation/.nvmrc | 1 + frontend/.nvmrc | 1 + 9 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 admin/.nvmrc create mode 100644 backend/.nvmrc create mode 100644 database/.nvmrc create mode 100644 dht-node/.nvmrc create mode 100644 dlt-connector/.nvmrc create mode 100644 dlt-database/.nvmrc create mode 100644 federation/.nvmrc create mode 100644 frontend/.nvmrc diff --git a/.gitignore b/.gitignore index 1a111760f..ab2ea22ee 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,5 @@ package-lock.json /deployment/bare_metal/log /deployment/bare_metal/backup -# Node Version Manager configuration file -.nvmrc - # Apple macOS folder attribute file .DS_Store diff --git a/admin/.nvmrc b/admin/.nvmrc new file mode 100644 index 000000000..c9d82507f --- /dev/null +++ b/admin/.nvmrc @@ -0,0 +1 @@ +v14.17.0 \ No newline at end of file diff --git a/backend/.nvmrc b/backend/.nvmrc new file mode 100644 index 000000000..79bdb1b97 --- /dev/null +++ b/backend/.nvmrc @@ -0,0 +1 @@ +v18.7.0 diff --git a/database/.nvmrc b/database/.nvmrc new file mode 100644 index 000000000..02c4afe7d --- /dev/null +++ b/database/.nvmrc @@ -0,0 +1 @@ +v18.7.0 \ No newline at end of file diff --git a/dht-node/.nvmrc b/dht-node/.nvmrc new file mode 100644 index 000000000..9dfdb2923 --- /dev/null +++ b/dht-node/.nvmrc @@ -0,0 +1 @@ +v19.5.0 \ No newline at end of file diff --git a/dlt-connector/.nvmrc b/dlt-connector/.nvmrc new file mode 100644 index 000000000..9dfdb2923 --- /dev/null +++ b/dlt-connector/.nvmrc @@ -0,0 +1 @@ +v19.5.0 \ No newline at end of file diff --git a/dlt-database/.nvmrc b/dlt-database/.nvmrc new file mode 100644 index 000000000..02c4afe7d --- /dev/null +++ b/dlt-database/.nvmrc @@ -0,0 +1 @@ +v18.7.0 \ No newline at end of file diff --git a/federation/.nvmrc b/federation/.nvmrc new file mode 100644 index 000000000..02c4afe7d --- /dev/null +++ b/federation/.nvmrc @@ -0,0 +1 @@ +v18.7.0 \ No newline at end of file diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..112a2eaed --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +lts/gallium \ No newline at end of file From 0c96b974571d90a2b5c0e0737c8270bb4a1a144b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 22 Feb 2024 09:06:42 +0100 Subject: [PATCH 39/51] fix lint --- dlt-connector/src/index.ts | 2 +- .../interactions/backendToDb/community/HomeCommunity.role.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 59e6782aa..bfbff10c3 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -5,12 +5,12 @@ 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 { LogError } from './server/LogError' -import { Mnemonic } from './data/Mnemonic' import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota' async function waitForServer( diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 31ecb8fc6..5d7bec94c 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -12,12 +12,12 @@ import { TransactionError } from '@/graphql/model/TransactionError' import { CommunityLoggingView } from '@/logging/CommunityLogging.view' import { logger } from '@/logging/logger' import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager' +import { LogError } from '@/server/LogError' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' import { CommunityRole } from './Community.role' -import { LogError } from '@/server/LogError' export class HomeCommunityRole extends CommunityRole { private transactionRecipe: Transaction From 15d12f1641f9a4a23e301cc1b5239156b39e17b1 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 22 Feb 2024 09:09:36 +0100 Subject: [PATCH 40/51] lower coverage --- dlt-connector/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 3d731787f..9b5a01350 100644 --- a/dlt-connector/jest.config.js +++ b/dlt-connector/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 75, + lines: 72, }, }, setupFiles: ['/test/testSetup.ts'], From d3c65cd7f86f7eb7c28a6a5c8d5b2bd03f9f22d0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 23 Feb 2024 09:18:43 +0100 Subject: [PATCH 41/51] make nginx restart automatic on failure --- deployment/hetzner_cloud/install.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh index 06b92ecaf..b51f9b454 100755 --- a/deployment/hetzner_cloud/install.sh +++ b/deployment/hetzner_cloud/install.sh @@ -104,6 +104,23 @@ ln -s $SCRIPT_PATH/nginx/common /etc/nginx/ rmdir /etc/nginx/conf.d 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 certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL From 03c566919cb59203b300d40ec40608b55e426f93 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 26 Feb 2024 14:06:13 +0100 Subject: [PATCH 42/51] add gms switch and name type --- .../src/graphql/arg/UpdateUserInfosArgs.ts | 12 +- backend/src/graphql/model/User.ts | 8 + backend/src/graphql/resolver/UserResolver.ts | 23 +-- .../UserSettings/UserGMSNamingFormat.vue | 84 ++++++++ .../components/UserSettings/UserGMSSwitch.vue | 45 ++++ frontend/src/graphql/mutations.js | 4 +- frontend/src/locales/de.json | 16 ++ frontend/src/locales/en.json | 16 ++ frontend/src/pages/Settings.vue | 192 +++++++++++------- frontend/src/store/store.js | 12 ++ 10 files changed, 317 insertions(+), 95 deletions(-) create mode 100644 frontend/src/components/UserSettings/UserGMSNamingFormat.vue create mode 100644 frontend/src/components/UserSettings/UserGMSSwitch.vue diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index 0920fb3bc..22b548f91 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,6 +1,7 @@ -import { IsBoolean, IsInt, IsString } from 'class-validator' +import { IsBoolean, IsEnum, IsInt, IsString } from 'class-validator' import { ArgsType, Field, InputType, Int } from 'type-graphql' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { Location } from '@model/Location' import { isValidLocation } from '@/graphql/validator/Location' @@ -48,9 +49,12 @@ export class UpdateUserInfosArgs { @IsBoolean() gmsAllowed?: boolean - @Field(() => Int, { nullable: true, defaultValue: 0 }) - @IsInt() - gmsPublishName?: number | null + @Field(() => GmsPublishNameType, { + nullable: true, + defaultValue: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + }) + @IsEnum(GmsPublishNameType) + gmsPublishName?: GmsPublishNameType | null @Field(() => Location, { nullable: true }) @isValidLocation() diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index d24a717c4..0d3cfccc7 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -29,6 +29,8 @@ export class User { this.hasElopage = null this.hideAmountGDD = user.hideAmountGDD this.hideAmountGDT = user.hideAmountGDT + this.gmsAllowed = user.gmsAllowed + this.gmsPublishName = user.gmsPublishName } } @@ -74,6 +76,12 @@ export class User { @Field(() => Boolean) hideAmountGDT: boolean + @Field(() => Boolean) + gmsAllowed: boolean + + @Field(() => Int, { nullable: true }) + gmsPublishName: number | null + // This is not the users publisherId, but the one of the users who recommend him @Field(() => Int, { nullable: true }) publisherId: number | null diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 3f70ce112..8f6b36652 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -561,17 +561,6 @@ export class UserResolver { logger.info( `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) // try { if (firstName) { @@ -620,12 +609,18 @@ export class UserResolver { user.hideAmountGDT = hideAmountGDT } - user.gmsAllowed = gmsAllowed - user.gmsPublishName = gmsPublishName + if (gmsAllowed) { + user.gmsAllowed = gmsAllowed + } + if (gmsPublishName) { + user.gmsPublishName = gmsPublishName + } if (gmsLocation) { user.location = Location2Point(gmsLocation) } - user.gmsPublishLocation = gmsPublishLocation + if (gmsPublishLocation) { + user.gmsPublishLocation = gmsPublishLocation + } // } catch (err) { // console.log('error:', err) // } diff --git a/frontend/src/components/UserSettings/UserGMSNamingFormat.vue b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue new file mode 100644 index 000000000..d12dde0ce --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/UserSettings/UserGMSSwitch.vue b/frontend/src/components/UserSettings/UserGMSSwitch.vue new file mode 100644 index 000000000..6958f4e57 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSSwitch.vue @@ -0,0 +1,45 @@ + + diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index cade098da..184a712ab 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -35,7 +35,7 @@ export const updateUserInfos = gql` $hideAmountGDD: Boolean $hideAmountGDT: Boolean $gmsAllowed: Boolean - $gmsPublishName: Int + $gmsPublishName: GmsPublishNameType $gmsLocation: Location $gmsPublishLocation: Int ) { @@ -172,6 +172,8 @@ export const login = gql` klickTipp { newsletterState } + gmsAllowed + gmsPublishName hasElopage publisherId roles diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 337722940..f1ba3b2f7 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -5,8 +5,10 @@ "1000thanks": "1000 Dank, weil du bei uns bist!", "125": "125%", "85": "85%", + "ExternServices": "Verknüpfte Dienste", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Karte", "PersonalDetails": "Persönliche Angaben", "advanced-calculation": "Vorausberechnung", "asterisks": "****", @@ -290,6 +292,20 @@ }, "settings": { "emailInfo": "Kann aktuell noch nicht geändert werden.", + "GMS": { + "disabled": "Daten werden nicht nach GMS exportiert", + "enabled": "Daten werden nach GMS exportiert", + "naming-format": "Namensformat im GMS", + "publish-name": { + "alias-or-initials": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname", + "first": "nur Vornamen", + "first-initial": "Vorname plus Anfangsbuchstabe des Nachnamens", + "initials": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens", + "name-full": "vollständiger Name: Vorname plus Nachname", + "updated": "Namensformat für GMS aktualisiert" + }, + "switch": "Erlaubnis Daten nach GMS zu exportieren." + }, "hideAmountGDD": "Dein GDD Betrag ist versteckt.", "hideAmountGDT": "Dein GDT Betrag ist versteckt.", "info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 982d22159..4756c00bf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -5,8 +5,10 @@ "1000thanks": "1000 thanks for being with us!", "125": "125%", "85": "85%", + "ExternServices": "Extern Services", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Map", "PersonalDetails": "Personal details", "advanced-calculation": "Advanced calculation", "asterisks": "****", @@ -290,6 +292,20 @@ }, "settings": { "emailInfo": "Cannot be changed at this time.", + "GMS": { + "disabled": "Data not exported to GMS", + "enabled": "Data exported to GMS", + "naming-format": "Format of name in GMS", + "publish-name": { + "alias-or-initials": "username if exists or Initials of firstname and lastname", + "first": "firstname only", + "first-initial": "firstname plus initial of lastname", + "initials": "Initials of firstname and lastname independent if username exists", + "name-full": "fullname: firstname plus lastname", + "updated": "format of name for GMS updated" + }, + "switch": "Allow data export to GMS" + }, "hideAmountGDD": "Your GDD amount is hidden.", "hideAmountGDT": "Your GDT amount is hidden.", "info": "Transactions can now be made by username or email address.", diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 1530e5e97..38a3317bf 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -1,81 +1,113 @@